resultar 2.0.0 → 3.0.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +437 -606
- package/dist/index.d.ts +622 -3
- package/dist/index.js +1007 -10
- package/dist/index.js.map +1 -1
- package/package.json +30 -60
- package/dist/no-discard.d.ts +0 -18
- package/dist/no-discard.js +0 -124
- package/dist/no-discard.js.map +0 -1
- package/dist/result-async-CdlVdXgX.js +0 -698
- package/dist/result-async-CdlVdXgX.js.map +0 -1
- package/dist/result-async-CxGBicja.d.ts +0 -439
package/README.md
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
# Resultar
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Production-facing error handling for TypeScript, without a runtime.
|
|
4
4
|
|
|
5
|
-
Resultar
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
Resultar models fallible work explicitly:
|
|
5
|
+
Resultar gives you typed `Result` primitives, real `Error`-based tagged failures, sync/async
|
|
6
|
+
composition, and optional no-discard diagnostics. It is small enough to adopt in one module and
|
|
7
|
+
strict enough to use at service, HTTP, job, queue, CLI, and integration boundaries.
|
|
10
8
|
|
|
11
9
|
```ts
|
|
12
10
|
Result<T, E>
|
|
@@ -15,22 +13,18 @@ StrictResult<T, E extends Error>
|
|
|
15
13
|
StrictResultAsync<T, E extends Error>
|
|
16
14
|
```
|
|
17
15
|
|
|
18
|
-
Use
|
|
19
|
-
tagged errors when you want strongly typed `Error` classes with stable tags and metadata.
|
|
20
|
-
Prefer `StrictResult` and `StrictResultAsync` at service boundaries so expected failures are real
|
|
21
|
-
`Error` instances with `message`, `cause`, stack traces, and structured metadata.
|
|
22
|
-
|
|
23
|
-
## Requirements
|
|
24
|
-
|
|
25
|
-
- ESM only
|
|
26
|
-
- Node.js 24+
|
|
27
|
-
- TypeScript 6-ready
|
|
16
|
+
Use it when expected failures should be impossible to miss:
|
|
28
17
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
18
|
+
- Function signatures show both the success type and the expected error type.
|
|
19
|
+
- Domain errors are real `Error` instances with stable tags, messages, `cause`, stack traces, and
|
|
20
|
+
JSON output.
|
|
21
|
+
- Sync and async fallible work compose without `try/catch` blocks scattered through application
|
|
22
|
+
code.
|
|
23
|
+
- Ignored `Result` values can be reported by a type-aware no-discard check.
|
|
32
24
|
|
|
33
|
-
|
|
25
|
+
Resultar began as an initial fork of `neverthrow`. The v3 alpha line keeps the explicit wrapper
|
|
26
|
+
model, then leans into Resultar-specific tagged errors, strict service-boundary types, TypeScript 6
|
|
27
|
+
support, and ESM-only packaging.
|
|
34
28
|
|
|
35
29
|
## Install
|
|
36
30
|
|
|
@@ -42,138 +36,40 @@ pnpm add resultar
|
|
|
42
36
|
npm install resultar
|
|
43
37
|
```
|
|
44
38
|
|
|
45
|
-
##
|
|
46
|
-
|
|
47
|
-
The `2.0.0` line removes deprecated APIs before stable release:
|
|
48
|
-
|
|
49
|
-
| Removed API | Use instead |
|
|
50
|
-
| --- | --- |
|
|
51
|
-
| `Result#finally(fn)` | `Result#log(fn)` for immediate side effects, or `Result#toDisposable(fn)` for Node.js `using` cleanup |
|
|
52
|
-
| `ResultAsync#finally(fn)` | `ResultAsync#log(fn)` for immediate side effects, or `ResultAsync#toAsyncDisposable(fn)` for Node.js `await using` cleanup |
|
|
53
|
-
| `ResultAsync#safeUnwrap()` | `yield* resultAsync` inside `safeTry(async function* () { ... })` |
|
|
54
|
-
| `safeTryAsync(...)` | `safeTry(async function* () { ... })` |
|
|
55
|
-
|
|
56
|
-
`Result#safeUnwrap()` remains available for compatibility, but new code should prefer `yield* result`.
|
|
57
|
-
|
|
58
|
-
## Philosophy
|
|
59
|
-
|
|
60
|
-
Resultar is a small explicit-result library, not an application runtime or an Effect-style framework.
|
|
61
|
-
It focuses on three things:
|
|
62
|
-
|
|
63
|
-
- Make expected failures visible in function signatures.
|
|
64
|
-
- Compose sync and async fallible work without `try/catch` control-flow blocks.
|
|
65
|
-
- Keep expected application failures observable by modeling them as real typed `Error` values.
|
|
66
|
-
|
|
67
|
-
The core model stays intentionally simple:
|
|
68
|
-
|
|
69
|
-
```ts
|
|
70
|
-
Result<T, E>
|
|
71
|
-
ResultAsync<T, E>
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
Use generic `Result<T, E>` when local code benefits from lightweight strings, enums, or domain
|
|
75
|
-
objects. Use `StrictResult<T, E extends Error>` and `StrictResultAsync<T, E extends Error>` when a
|
|
76
|
-
failure can cross a module, service, HTTP, job, or integration boundary.
|
|
77
|
-
|
|
78
|
-
## Recommended Production Style
|
|
79
|
-
|
|
80
|
-
For backend and application code, prefer this stack:
|
|
39
|
+
## Requirements
|
|
81
40
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
4. Map outcomes at boundaries with `matchTags` or `matchTagsPartial`.
|
|
86
|
-
5. Use `tap`, `tapError`, and `log` only for best-effort side effects.
|
|
41
|
+
- Node.js 24+
|
|
42
|
+
- TypeScript 6+
|
|
43
|
+
- ESM only
|
|
87
44
|
|
|
88
45
|
```ts
|
|
89
|
-
import type { StrictResult } from 'resultar'
|
|
90
|
-
|
|
91
46
|
import { createTaggedError, ok } from 'resultar'
|
|
92
|
-
|
|
93
|
-
class UserAlreadyExistsError extends createTaggedError({
|
|
94
|
-
name: 'UserAlreadyExistsError',
|
|
95
|
-
message: 'User $email already exists',
|
|
96
|
-
}) {}
|
|
97
|
-
|
|
98
|
-
const ensureUserDoesNotExist = (
|
|
99
|
-
email: string,
|
|
100
|
-
): StrictResult<string, UserAlreadyExistsError> =>
|
|
101
|
-
email === 'taken@example.com' ? UserAlreadyExistsError.err({ email }) : ok(email)
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
Avoid using strings or loose objects for production-facing expected failures:
|
|
105
|
-
|
|
106
|
-
```ts
|
|
107
|
-
// Local-only style. Avoid at service boundaries.
|
|
108
|
-
type ParseError = 'InvalidPort'
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
### No-Discard Check
|
|
112
|
-
|
|
113
|
-
Resultar ships a type-aware check for discarded `Result` and `ResultAsync` values:
|
|
114
|
-
|
|
115
|
-
```json
|
|
116
|
-
{
|
|
117
|
-
"scripts": {
|
|
118
|
-
"lint:resultar": "resultar-no-discard --project tsconfig.json"
|
|
119
|
-
}
|
|
120
|
-
}
|
|
47
|
+
import type { StrictResult } from 'resultar'
|
|
121
48
|
```
|
|
122
49
|
|
|
123
|
-
|
|
50
|
+
CommonJS `require('resultar')` is not exported.
|
|
124
51
|
|
|
125
|
-
|
|
126
|
-
saveUser(input)
|
|
127
|
-
```
|
|
52
|
+
## Why Teams Use It
|
|
128
53
|
|
|
129
|
-
|
|
54
|
+
This signature hides an expected failure:
|
|
130
55
|
|
|
131
56
|
```ts
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
void saveUser(input)
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
For Oxlint users, run it next to Oxlint because this rule needs TypeScript return-type information:
|
|
57
|
+
const parsePort = (value: string): number => {
|
|
58
|
+
const port = Number(value)
|
|
138
59
|
|
|
139
|
-
|
|
140
|
-
{
|
|
141
|
-
"scripts": {
|
|
142
|
-
"lint": "oxlint && resultar-no-discard --project tsconfig.json"
|
|
60
|
+
if (!Number.isInteger(port) || port <= 0) {
|
|
61
|
+
throw new Error(`Invalid port ${value}`)
|
|
143
62
|
}
|
|
63
|
+
|
|
64
|
+
return port
|
|
144
65
|
}
|
|
145
66
|
```
|
|
146
67
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
Use `map` when the `Ok` value changes and the transform cannot fail.
|
|
150
|
-
|
|
151
|
-
Use `andThen` when the next step can fail and returns another `Result`. Use `asyncAndThen` when a
|
|
152
|
-
sync `Result` continues into `ResultAsync`.
|
|
153
|
-
|
|
154
|
-
Use `mapErr` when the error value changes. Use `orElse` when an error should recover into another
|
|
155
|
-
`Result`.
|
|
156
|
-
|
|
157
|
-
Use `catchTag` or `catchTags` for local recovery from tagged errors. `catchTags` is intentionally
|
|
158
|
-
partial: unhandled tags stay in the error channel.
|
|
159
|
-
|
|
160
|
-
Use `match` for simple boundaries. Use `matchTags` when every tagged error must be handled. Use
|
|
161
|
-
`matchTagsPartial` when only selected tagged errors need custom handling and the rest should go to a
|
|
162
|
-
fallback.
|
|
163
|
-
|
|
164
|
-
Use `fromPromise(promise, toError)` when you already have a promise. Use
|
|
165
|
-
`tryCatchAsync(() => promiseFactory(), toError)` when creating the promise can throw synchronously.
|
|
166
|
-
|
|
167
|
-
Use `tryCatch` / `tryCatchAsync` at the edge of uncontrolled code: JSON parsing, third-party
|
|
168
|
-
libraries, I/O, network calls, and other APIs that throw or reject. Keep your own domain functions
|
|
169
|
-
returning `Result` values instead of throwing expected failures.
|
|
170
|
-
|
|
171
|
-
## Quick Start
|
|
68
|
+
This signature makes the failure part of the contract:
|
|
172
69
|
|
|
173
70
|
```ts
|
|
174
|
-
import type { StrictResult } from 'resultar'
|
|
175
|
-
|
|
176
71
|
import { createTaggedError, ok } from 'resultar'
|
|
72
|
+
import type { StrictResult } from 'resultar'
|
|
177
73
|
|
|
178
74
|
class InvalidPortError extends createTaggedError({
|
|
179
75
|
name: 'InvalidPortError',
|
|
@@ -183,163 +79,134 @@ class InvalidPortError extends createTaggedError({
|
|
|
183
79
|
const parsePort = (value: string): StrictResult<number, InvalidPortError> => {
|
|
184
80
|
const port = Number(value)
|
|
185
81
|
|
|
186
|
-
return Number.isInteger(port) && port > 0
|
|
187
|
-
? ok(port)
|
|
188
|
-
: InvalidPortError.err({ value })
|
|
82
|
+
return Number.isInteger(port) && port > 0 ? ok(port) : InvalidPortError.err({ value })
|
|
189
83
|
}
|
|
190
|
-
|
|
191
|
-
const message = parsePort('3000').match(
|
|
192
|
-
(port) => `Listening on ${port}`,
|
|
193
|
-
(error) => error.message,
|
|
194
|
-
)
|
|
195
84
|
```
|
|
196
85
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
`StrictResult<T, E extends Error>` and `StrictResultAsync<T, E extends Error>` are type-only aliases
|
|
200
|
-
for code that should return only `Error`-based failures.
|
|
201
|
-
|
|
202
|
-
```ts
|
|
203
|
-
import type { StrictResult } from 'resultar'
|
|
204
|
-
|
|
205
|
-
import { createTaggedError, ok } from 'resultar'
|
|
206
|
-
|
|
207
|
-
class InvalidEmailError extends createTaggedError({
|
|
208
|
-
name: 'InvalidEmailError',
|
|
209
|
-
message: 'Invalid email $email',
|
|
210
|
-
}) {}
|
|
86
|
+
Callers now know the success type and the expected error type without reading the implementation.
|
|
87
|
+
That is the main trade: expected failures become visible contracts instead of tribal knowledge.
|
|
211
88
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
```
|
|
89
|
+
Resultar is intentionally not an application runtime. It gives you explicit error values that work
|
|
90
|
+
in normal TypeScript.
|
|
215
91
|
|
|
216
|
-
|
|
217
|
-
are useful. Prefer `StrictResult` for application and backend service boundaries where logging,
|
|
218
|
-
`cause`, stack traces, and tagged-error matching matter.
|
|
92
|
+
## What You Get
|
|
219
93
|
|
|
220
|
-
|
|
94
|
+
| Problem | Resultar gives you |
|
|
95
|
+
| --- | --- |
|
|
96
|
+
| Expected failures are hidden behind `throw` | `Result<T, E>` and `ResultAsync<T, E>` in function signatures |
|
|
97
|
+
| Production errors need stable names and metadata | `createTaggedError` classes with `_tag`, template props, `cause`, and `.toJSON()` |
|
|
98
|
+
| Async flows turn into nested `try/catch` | `tryResultAsync`, `andThen`, `asyncAndThen`, `map`, `orElse`, `safeTry` |
|
|
99
|
+
| Boundary handlers miss cases | `matchTags` for exhaustive tagged-error handling |
|
|
100
|
+
| Results are easy to ignore | `resultar-no-discard` and the `resultar-ls` TypeScript plugin |
|
|
101
|
+
| You do not want another application runtime | A small ESM library around explicit values |
|
|
221
102
|
|
|
222
|
-
|
|
223
|
-
through `StrictResult` or `StrictResultAsync`.
|
|
103
|
+
Nested `try/catch` often masks the error the caller actually needs:
|
|
224
104
|
|
|
225
105
|
```ts
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
}) {}
|
|
230
|
-
```
|
|
106
|
+
const getPosts = async (url: string) => {
|
|
107
|
+
try {
|
|
108
|
+
const response = await fetch(url)
|
|
231
109
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
structured logging, and boundary matching do not matter.
|
|
110
|
+
if (response.status >= 400) {
|
|
111
|
+
throw new HttpClientError(response.status)
|
|
112
|
+
}
|
|
236
113
|
|
|
237
|
-
|
|
114
|
+
return response.json() as Promise<Post[]>
|
|
115
|
+
} catch (cause) {
|
|
116
|
+
throw new InfrastructureError({ cause })
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
238
120
|
|
|
239
|
-
|
|
121
|
+
Resultar keeps each failure explicit and typed:
|
|
240
122
|
|
|
241
123
|
```ts
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
id === 'usr_123' ? ok({ id }) : UserNotFoundError.err({ id })
|
|
253
|
-
|
|
254
|
-
const result = findUser('usr_123')
|
|
255
|
-
|
|
256
|
-
if (result.isOk()) {
|
|
257
|
-
result.value.id
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
if (result.isErr()) {
|
|
261
|
-
result.error.message
|
|
262
|
-
}
|
|
124
|
+
const getPosts = (
|
|
125
|
+
url: string,
|
|
126
|
+
): StrictResultAsync<Post[], FetchPostsError | HttpClientError | ParsePostsError> =>
|
|
127
|
+
tryResultAsync(fetch(url), (cause) => new FetchPostsError({ cause }))
|
|
128
|
+
.andThen((response) =>
|
|
129
|
+
response.status >= 400 ? HttpClientError.err({ status: response.status }) : ok(response),
|
|
130
|
+
)
|
|
131
|
+
.andThen((response) =>
|
|
132
|
+
tryResultAsync(response.json() as Promise<Post[]>, (cause) => new ParsePostsError({ cause })),
|
|
133
|
+
)
|
|
263
134
|
```
|
|
264
135
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
Use `map` for infallible transforms and `andThen` for fallible transforms.
|
|
136
|
+
## Quick Start
|
|
268
137
|
|
|
269
138
|
```ts
|
|
270
|
-
import type { StrictResult } from 'resultar'
|
|
271
|
-
|
|
272
139
|
import { createTaggedError, ok } from 'resultar'
|
|
273
|
-
|
|
274
|
-
type User = {
|
|
275
|
-
readonly email: string
|
|
276
|
-
}
|
|
140
|
+
import type { StrictResult } from 'resultar'
|
|
277
141
|
|
|
278
142
|
class InvalidEmailError extends createTaggedError({
|
|
279
143
|
name: 'InvalidEmailError',
|
|
280
144
|
message: 'Invalid email $email',
|
|
281
145
|
}) {}
|
|
282
146
|
|
|
283
|
-
class
|
|
284
|
-
name: '
|
|
285
|
-
message: '
|
|
147
|
+
class UserAlreadyExistsError extends createTaggedError({
|
|
148
|
+
name: 'UserAlreadyExistsError',
|
|
149
|
+
message: 'User $email already exists',
|
|
286
150
|
}) {}
|
|
287
151
|
|
|
152
|
+
type User = {
|
|
153
|
+
readonly email: string
|
|
154
|
+
readonly id: string
|
|
155
|
+
}
|
|
156
|
+
|
|
288
157
|
const validateEmail = (email: string): StrictResult<string, InvalidEmailError> =>
|
|
289
158
|
email.includes('@') ? ok(email) : InvalidEmailError.err({ email })
|
|
290
159
|
|
|
291
|
-
const
|
|
292
|
-
validateEmail(email).map((validEmail) => ({ email: validEmail }))
|
|
293
|
-
```
|
|
294
|
-
|
|
295
|
-
Use `filterOrElse` when a successful value must satisfy an extra predicate:
|
|
296
|
-
|
|
297
|
-
```ts
|
|
298
|
-
const validateCompanyEmail = (
|
|
160
|
+
const ensureUserDoesNotExist = (
|
|
299
161
|
email: string,
|
|
300
|
-
): StrictResult<string,
|
|
301
|
-
|
|
302
|
-
(validEmail) => validEmail.endsWith('@company.com'),
|
|
303
|
-
(validEmail) => new InvalidDomainError({ domain: validEmail.split('@')[1] ?? 'unknown' }),
|
|
304
|
-
)
|
|
305
|
-
```
|
|
306
|
-
|
|
307
|
-
Generic `Result<T, E>` still supports strings, enums, and lightweight objects for narrow local flows.
|
|
308
|
-
For application and backend service boundaries, prefer `StrictResult<T, E extends Error>` with
|
|
309
|
-
`createTaggedError`.
|
|
162
|
+
): StrictResult<string, UserAlreadyExistsError> =>
|
|
163
|
+
email === 'taken@example.com' ? UserAlreadyExistsError.err({ email }) : ok(email)
|
|
310
164
|
|
|
311
|
-
|
|
165
|
+
const insertUser = (email: string): StrictResult<User, never> =>
|
|
166
|
+
ok({ email, id: 'usr_123' })
|
|
312
167
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
168
|
+
const createUser = (
|
|
169
|
+
email: string,
|
|
170
|
+
): StrictResult<User, InvalidEmailError | UserAlreadyExistsError> =>
|
|
171
|
+
validateEmail(email).andThen(ensureUserDoesNotExist).andThen(insertUser)
|
|
317
172
|
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
173
|
+
const response = createUser('taken@example.com').matchTags(
|
|
174
|
+
(user) => ({ body: user, statusCode: 201 }),
|
|
175
|
+
{
|
|
176
|
+
InvalidEmailError: (error) => ({
|
|
177
|
+
body: { code: error._tag, message: error.message },
|
|
178
|
+
statusCode: 400,
|
|
179
|
+
}),
|
|
180
|
+
UserAlreadyExistsError: (error) => ({
|
|
181
|
+
body: { code: error._tag, message: error.message },
|
|
182
|
+
statusCode: 409,
|
|
183
|
+
}),
|
|
184
|
+
},
|
|
321
185
|
)
|
|
322
186
|
```
|
|
323
187
|
|
|
324
|
-
|
|
188
|
+
## Core Model
|
|
325
189
|
|
|
326
|
-
|
|
190
|
+
`Result<T, E>` is a synchronous fallible value. It is either an `Ok<T>` or an `Err<E>`.
|
|
327
191
|
|
|
328
192
|
```ts
|
|
329
|
-
const
|
|
330
|
-
(user) => ({ body: user, statusCode: 201 }),
|
|
331
|
-
(error) => ({ body: { error }, statusCode: 400 }),
|
|
332
|
-
)
|
|
333
|
-
```
|
|
193
|
+
const result = parsePort('3000')
|
|
334
194
|
|
|
335
|
-
|
|
195
|
+
if (result.isOk()) {
|
|
196
|
+
result.value
|
|
197
|
+
}
|
|
336
198
|
|
|
337
|
-
|
|
199
|
+
if (result.isErr()) {
|
|
200
|
+
result.error
|
|
201
|
+
}
|
|
202
|
+
```
|
|
338
203
|
|
|
339
|
-
|
|
340
|
-
|
|
204
|
+
`ResultAsync<T, E>` is the async version. It wraps promise-based fallible work while keeping the
|
|
205
|
+
same composition API.
|
|
341
206
|
|
|
342
|
-
|
|
207
|
+
```ts
|
|
208
|
+
import { createTaggedError, fromPromise, ok, tryResultAsync } from 'resultar'
|
|
209
|
+
import type { StrictResult, StrictResultAsync } from 'resultar'
|
|
343
210
|
|
|
344
211
|
type User = {
|
|
345
212
|
readonly id: string
|
|
@@ -350,11 +217,48 @@ class FetchUserError extends createTaggedError({
|
|
|
350
217
|
message: 'Failed to fetch user $id',
|
|
351
218
|
}) {}
|
|
352
219
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
220
|
+
class UserResponseError extends createTaggedError({
|
|
221
|
+
name: 'UserResponseError',
|
|
222
|
+
message: 'User request $id failed with status $status',
|
|
223
|
+
}) {}
|
|
224
|
+
|
|
225
|
+
class ParseUserError extends createTaggedError({
|
|
226
|
+
name: 'ParseUserError',
|
|
227
|
+
message: 'Failed to parse user $id response',
|
|
228
|
+
}) {}
|
|
229
|
+
|
|
230
|
+
class DecodeUserError extends createTaggedError({
|
|
231
|
+
name: 'DecodeUserError',
|
|
232
|
+
message: 'Invalid user $id response payload',
|
|
233
|
+
}) {}
|
|
234
|
+
|
|
235
|
+
const decodeUser = (input: unknown, id: string): StrictResult<User, DecodeUserError> => {
|
|
236
|
+
if (
|
|
237
|
+
typeof input === 'object' &&
|
|
238
|
+
input !== null &&
|
|
239
|
+
'id' in input &&
|
|
240
|
+
typeof input.id === 'string'
|
|
241
|
+
) {
|
|
242
|
+
return ok({ id: input.id })
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return DecodeUserError.err({ id })
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const fetchUser = (
|
|
249
|
+
id: string,
|
|
250
|
+
): StrictResultAsync<User, FetchUserError | UserResponseError | ParseUserError | DecodeUserError> =>
|
|
251
|
+
fromPromise(fetch(`https://example.com/users/${id}`), (cause) => new FetchUserError({ cause, id }))
|
|
252
|
+
.andThen((response) =>
|
|
253
|
+
response.ok ? ok(response) : UserResponseError.err({ id, status: response.status }),
|
|
254
|
+
)
|
|
255
|
+
.andThen((response) =>
|
|
256
|
+
tryResultAsync<unknown, ParseUserError>(
|
|
257
|
+
() => response.json(),
|
|
258
|
+
(cause) => new ParseUserError({ cause, id }),
|
|
259
|
+
),
|
|
260
|
+
)
|
|
261
|
+
.andThen((input) => decodeUser(input, id))
|
|
358
262
|
|
|
359
263
|
const label = await fetchUser('usr_123').match(
|
|
360
264
|
(user) => `User ${user.id}`,
|
|
@@ -362,19 +266,19 @@ const label = await fetchUser('usr_123').match(
|
|
|
362
266
|
)
|
|
363
267
|
```
|
|
364
268
|
|
|
365
|
-
|
|
269
|
+
This keeps network failures, non-2xx responses, JSON parse failures, and payload decode failures
|
|
270
|
+
separate. In production, treat parsing bytes into `unknown` and decoding `unknown` into a domain
|
|
271
|
+
type as different failure modes.
|
|
366
272
|
|
|
367
|
-
|
|
273
|
+
Use generic `Result<T, E>` for local flows where strings, enums, or small domain objects are useful
|
|
274
|
+
as `E`. Prefer `StrictResult<T, E extends Error>` and
|
|
275
|
+
`StrictResultAsync<T, E extends Error>` at service, HTTP, job, queue, CLI, and integration
|
|
276
|
+
boundaries.
|
|
368
277
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
- `cause`
|
|
374
|
-
- `.toJSON()`
|
|
375
|
-
- `.findCause(ErrorClass)`
|
|
376
|
-
- static `.is(value)` using `instanceof` for nominal class checks
|
|
377
|
-
- static `.err(props)`
|
|
278
|
+
## Tagged Errors
|
|
279
|
+
|
|
280
|
+
`createTaggedError` creates real `Error` subclasses with stable tags and typed message-template
|
|
281
|
+
props.
|
|
378
282
|
|
|
379
283
|
```ts
|
|
380
284
|
import { createTaggedError } from 'resultar'
|
|
@@ -397,7 +301,19 @@ error.id // string | number
|
|
|
397
301
|
error.source // string | number
|
|
398
302
|
```
|
|
399
303
|
|
|
400
|
-
|
|
304
|
+
Tagged errors include:
|
|
305
|
+
|
|
306
|
+
- `_tag`
|
|
307
|
+
- `messageTemplate`
|
|
308
|
+
- `fingerprint`
|
|
309
|
+
- `cause`
|
|
310
|
+
- `.toJSON()`
|
|
311
|
+
- `.findCause(ErrorClass)`
|
|
312
|
+
- static `.is(value)` for nominal checks
|
|
313
|
+
- static `.err(props)` for returning `Err`
|
|
314
|
+
|
|
315
|
+
Template variables are inferred from `$variables` in the message. Missing variables are TypeScript
|
|
316
|
+
errors.
|
|
401
317
|
|
|
402
318
|
```ts
|
|
403
319
|
new UserNotFoundError({ id: 'usr_123', source: 'database' })
|
|
@@ -406,18 +322,7 @@ new UserNotFoundError({ id: 'usr_123', source: 'database' })
|
|
|
406
322
|
new UserNotFoundError({ id: 'usr_123' })
|
|
407
323
|
```
|
|
408
324
|
|
|
409
|
-
|
|
410
|
-
metadata:
|
|
411
|
-
|
|
412
|
-
```ts
|
|
413
|
-
// @ts-expect-error name is reserved
|
|
414
|
-
createTaggedError({ name: 'BadError', message: 'Bad $name' })
|
|
415
|
-
```
|
|
416
|
-
|
|
417
|
-
Reserved variables are `_tag`, `name`, `message`, `messageTemplate`, `fingerprint`, `stack`, and
|
|
418
|
-
`cause`.
|
|
419
|
-
|
|
420
|
-
For lightweight tagged domain unions that do not need to extend `Error`, use `TaggedEnum`:
|
|
325
|
+
Use `TaggedEnum` only for lightweight tagged unions that do not need to be real errors.
|
|
421
326
|
|
|
422
327
|
```ts
|
|
423
328
|
import type { TaggedEnum } from 'resultar'
|
|
@@ -426,435 +331,365 @@ type PaymentError = TaggedEnum<{
|
|
|
426
331
|
CardDeclined: { readonly code: string }
|
|
427
332
|
InsufficientFunds: { readonly balance: number }
|
|
428
333
|
}>
|
|
429
|
-
|
|
430
|
-
const error: PaymentError = { _tag: 'CardDeclined', code: 'card_declined' }
|
|
431
334
|
```
|
|
432
335
|
|
|
433
|
-
##
|
|
336
|
+
## Composing Results
|
|
434
337
|
|
|
435
|
-
|
|
436
|
-
`StrictResultAsync<T, E>`.
|
|
338
|
+
Use `map` for infallible transforms:
|
|
437
339
|
|
|
438
340
|
```ts
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
import { createTaggedError, ok } from 'resultar'
|
|
442
|
-
|
|
443
|
-
class InvalidEmailError extends createTaggedError({
|
|
444
|
-
name: 'InvalidEmailError',
|
|
445
|
-
message: 'Invalid email $email',
|
|
446
|
-
}) {}
|
|
447
|
-
|
|
448
|
-
class UserAlreadyExistsError extends createTaggedError({
|
|
449
|
-
name: 'UserAlreadyExistsError',
|
|
450
|
-
message: 'User $email already exists',
|
|
451
|
-
}) {}
|
|
452
|
-
|
|
453
|
-
class DatabaseError extends createTaggedError({
|
|
454
|
-
name: 'DatabaseError',
|
|
455
|
-
message: 'Database operation $operation failed',
|
|
456
|
-
}) {}
|
|
457
|
-
|
|
458
|
-
type User = {
|
|
459
|
-
readonly email: string
|
|
460
|
-
readonly id: string
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
type CreateUserError = InvalidEmailError | UserAlreadyExistsError | DatabaseError
|
|
464
|
-
|
|
465
|
-
const validateEmail = (email: string): StrictResult<string, InvalidEmailError> =>
|
|
466
|
-
email.includes('@') ? ok(email) : InvalidEmailError.err({ email })
|
|
341
|
+
const normalized = validateEmail('PERSON@EXAMPLE.COM').map((email) => email.toLowerCase())
|
|
342
|
+
```
|
|
467
343
|
|
|
468
|
-
|
|
469
|
-
email: string,
|
|
470
|
-
): StrictResult<string, UserAlreadyExistsError | DatabaseError> => {
|
|
471
|
-
if (email === 'taken@example.com') {
|
|
472
|
-
return UserAlreadyExistsError.err({ email })
|
|
473
|
-
}
|
|
344
|
+
Use `as` when success matters but the previous value does not:
|
|
474
345
|
|
|
475
|
-
|
|
476
|
-
}
|
|
346
|
+
```ts
|
|
347
|
+
const created = insertUser(user).as({ status: 'created' as const })
|
|
348
|
+
```
|
|
477
349
|
|
|
478
|
-
|
|
479
|
-
ok({ email, id: 'usr_123' })
|
|
350
|
+
Use `andThen` when the next step can fail:
|
|
480
351
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
.andThen(ensureUserDoesNotExist)
|
|
484
|
-
.andThen(insertUser)
|
|
352
|
+
```ts
|
|
353
|
+
const user = validateEmail(email).andThen(ensureUserDoesNotExist).andThen(insertUser)
|
|
485
354
|
```
|
|
486
355
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
Use `catchTag` when one tagged error can be recovered locally:
|
|
356
|
+
Use `asyncAndThen` when a sync result continues into async work:
|
|
490
357
|
|
|
491
358
|
```ts
|
|
492
|
-
const
|
|
493
|
-
ok({
|
|
494
|
-
email: error.email,
|
|
495
|
-
id: 'draft_user',
|
|
496
|
-
}),
|
|
497
|
-
)
|
|
359
|
+
const user = validateEmail(email).asyncAndThen(ensureUserDoesNotExistAsync).andThen(insertUserAsync)
|
|
498
360
|
```
|
|
499
361
|
|
|
500
|
-
Use `
|
|
362
|
+
Use `filterOrElse` when an `Ok` value must satisfy another predicate:
|
|
501
363
|
|
|
502
364
|
```ts
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
})
|
|
507
|
-
```
|
|
365
|
+
class InvalidDomainError extends createTaggedError({
|
|
366
|
+
name: 'InvalidDomainError',
|
|
367
|
+
message: 'Invalid email domain $domain',
|
|
368
|
+
}) {}
|
|
508
369
|
|
|
509
|
-
|
|
510
|
-
|
|
370
|
+
const validateCompanyEmail = (
|
|
371
|
+
email: string,
|
|
372
|
+
): StrictResult<string, InvalidEmailError | InvalidDomainError> =>
|
|
373
|
+
validateEmail(email).filterOrElse(
|
|
374
|
+
(validEmail) => validEmail.endsWith('@company.com'),
|
|
375
|
+
(validEmail) => new InvalidDomainError({ domain: validEmail.split('@')[1] ?? 'unknown' }),
|
|
376
|
+
)
|
|
377
|
+
```
|
|
511
378
|
|
|
512
|
-
|
|
379
|
+
Use `catchTag` or `catchTags` for local recovery from tagged errors:
|
|
513
380
|
|
|
514
381
|
```ts
|
|
515
|
-
const
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
body: user,
|
|
520
|
-
statusCode: 201,
|
|
382
|
+
const draftUser = createUser('bad-email').catchTag('InvalidEmailError', (error) =>
|
|
383
|
+
ok({
|
|
384
|
+
email: error.email,
|
|
385
|
+
id: 'draft_user',
|
|
521
386
|
}),
|
|
522
|
-
{
|
|
523
|
-
InvalidEmailError: (err) => ({
|
|
524
|
-
body: { code: err._tag, message: err.message },
|
|
525
|
-
statusCode: 400,
|
|
526
|
-
}),
|
|
527
|
-
UserAlreadyExistsError: (err) => ({
|
|
528
|
-
body: { code: err._tag, message: err.message },
|
|
529
|
-
statusCode: 409,
|
|
530
|
-
}),
|
|
531
|
-
DatabaseError: (err) => ({
|
|
532
|
-
body: { code: err._tag, message: err.message },
|
|
533
|
-
statusCode: 500,
|
|
534
|
-
}),
|
|
535
|
-
},
|
|
536
387
|
)
|
|
537
388
|
```
|
|
538
389
|
|
|
539
|
-
|
|
540
|
-
|
|
390
|
+
Use `match`, `matchTags`, or `matchTagsPartial` at boundaries where a result becomes a response,
|
|
391
|
+
log entry, queue acknowledgement, CLI exit code, or UI state.
|
|
541
392
|
|
|
542
393
|
```ts
|
|
543
|
-
const response =
|
|
544
|
-
(user) => ({
|
|
545
|
-
body: user,
|
|
546
|
-
statusCode: 201,
|
|
547
|
-
}),
|
|
394
|
+
const response = createUser(input.email).matchTagsPartial(
|
|
395
|
+
(user) => ({ body: user, statusCode: 201 }),
|
|
548
396
|
{
|
|
549
|
-
InvalidEmailError: (
|
|
550
|
-
body: { code:
|
|
397
|
+
InvalidEmailError: (error) => ({
|
|
398
|
+
body: { code: error._tag, message: error.message },
|
|
551
399
|
statusCode: 400,
|
|
552
400
|
}),
|
|
553
401
|
},
|
|
554
|
-
(
|
|
555
|
-
body: { code: 'UnhandledError', message:
|
|
402
|
+
(error) => ({
|
|
403
|
+
body: { code: 'UnhandledError', message: error.message },
|
|
556
404
|
statusCode: 500,
|
|
557
405
|
}),
|
|
558
406
|
)
|
|
559
407
|
```
|
|
560
408
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
When the error union includes plain or untagged `Error`, add an `Error` fallback. Use standalone `matchError` when you only have an error value rather than a `Result`.
|
|
409
|
+
## Fallbacks
|
|
564
410
|
|
|
565
|
-
|
|
566
|
-
declare const error: InvalidEmailError | DatabaseError | Error
|
|
567
|
-
|
|
568
|
-
const message = matchError(error, {
|
|
569
|
-
DatabaseError: (err) => err.message,
|
|
570
|
-
Error: (err) => err.message,
|
|
571
|
-
InvalidEmailError: (err) => err.message,
|
|
572
|
-
})
|
|
573
|
-
```
|
|
411
|
+
Use fallback APIs when an expected failure has a useful recovery path.
|
|
574
412
|
|
|
575
|
-
|
|
413
|
+
| Effect fallback idea | Resultar equivalent |
|
|
414
|
+
| --- | --- |
|
|
415
|
+
| try another effect | `result.orElse(() => fallbackResult)` |
|
|
416
|
+
| replace the failure | `mapErr(...)` or `orElse(() => err(newError))` |
|
|
417
|
+
| replace the failure with success | `orElse(() => ok(defaultValue))` inside a pipeline |
|
|
418
|
+
| default at the edge | `unwrapOr(defaultValue)` |
|
|
419
|
+
| try ordered candidates | `Result.firstSuccessOf` or `ResultAsync.firstSuccessOf` |
|
|
576
420
|
|
|
577
|
-
Use `
|
|
421
|
+
Use `orElse` for pipeline recovery:
|
|
578
422
|
|
|
579
423
|
```ts
|
|
580
|
-
|
|
424
|
+
const user = readUserFromCache(id).orElse(() => readUserFromRemote(id))
|
|
425
|
+
```
|
|
581
426
|
|
|
582
|
-
|
|
427
|
+
Use `mapErr` when a low-level failure should become a domain failure:
|
|
583
428
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
{
|
|
587
|
-
InvalidEmailError: (err) => `Invalid input: ${err.email}`,
|
|
588
|
-
},
|
|
589
|
-
(err) => err.message,
|
|
429
|
+
```ts
|
|
430
|
+
const user = readUserFromRemote(id).mapErr(
|
|
431
|
+
(cause) => new ReadUserError({ cause, id }),
|
|
590
432
|
)
|
|
591
433
|
```
|
|
592
434
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
Tagged errors support native `cause` and cause-chain lookup.
|
|
435
|
+
Use `orElse(() => ok(defaultValue))` when fallback success should stay inside the pipeline:
|
|
596
436
|
|
|
597
437
|
```ts
|
|
598
|
-
|
|
438
|
+
const user = readUser(id).orElse(() =>
|
|
439
|
+
ok({
|
|
440
|
+
email: 'guest@example.com',
|
|
441
|
+
id: 'guest',
|
|
442
|
+
}),
|
|
443
|
+
)
|
|
444
|
+
```
|
|
599
445
|
|
|
600
|
-
|
|
601
|
-
const error = new DatabaseError({
|
|
602
|
-
cause,
|
|
603
|
-
operation: 'insert-user',
|
|
604
|
-
})
|
|
446
|
+
Use `firstSuccessOf` when each fallback should run only if earlier candidates fail:
|
|
605
447
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
448
|
+
```ts
|
|
449
|
+
const user = ResultAsync.firstSuccessOf([
|
|
450
|
+
() => readUserFromCache(id),
|
|
451
|
+
() => readUserFromPrimary(id),
|
|
452
|
+
() => readUserFromReplica(id),
|
|
453
|
+
])
|
|
609
454
|
```
|
|
610
455
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
## Safe Try
|
|
456
|
+
Use `catchTag` / `catchTags` when recovery depends on a specific tagged error. Use
|
|
457
|
+
`matchTagsPartial` when only a boundary needs fallback mapping. Use `unwrapOr` only at final edges
|
|
458
|
+
where defaulting is intentional.
|
|
615
459
|
|
|
616
|
-
|
|
460
|
+
## Wrapping Throwing Code
|
|
617
461
|
|
|
618
|
-
|
|
462
|
+
Use `tryResult` and `tryResultAsync` at the edge of uncontrolled code:
|
|
463
|
+
JSON parsing, platform APIs, third-party libraries, I/O, and network calls.
|
|
619
464
|
|
|
620
465
|
```ts
|
|
621
|
-
import
|
|
622
|
-
|
|
623
|
-
import { ok, safeTry } from 'resultar'
|
|
466
|
+
import { tryResult } from 'resultar'
|
|
624
467
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
const loadPort = (): Result<number, Error> =>
|
|
630
|
-
safeTry(function* () {
|
|
631
|
-
const config = yield* readConfig()
|
|
632
|
-
const port = yield* parsePort(config.port)
|
|
633
|
-
|
|
634
|
-
return ok(port)
|
|
635
|
-
})
|
|
468
|
+
class ParseConfigError extends createTaggedError({
|
|
469
|
+
name: 'ParseConfigError',
|
|
470
|
+
message: 'Could not parse config',
|
|
471
|
+
}) {}
|
|
636
472
|
|
|
637
|
-
const
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
473
|
+
const parseConfig = (input: string): StrictResult<Config, ParseConfigError> =>
|
|
474
|
+
tryResult(
|
|
475
|
+
() => JSON.parse(input) as Config,
|
|
476
|
+
(cause) => new ParseConfigError({ cause }),
|
|
477
|
+
)
|
|
478
|
+
```
|
|
641
479
|
|
|
642
|
-
|
|
480
|
+
Use `fromPromise` when you already have a promise and need to map rejection into an expected error:
|
|
643
481
|
|
|
644
|
-
|
|
645
|
-
|
|
482
|
+
```ts
|
|
483
|
+
const user = fromPromise(loadUserFromRemote(id), (cause) => new FetchUserError({ cause, id }))
|
|
646
484
|
```
|
|
647
485
|
|
|
648
|
-
|
|
486
|
+
After that edge conversion, keep your own domain functions returning `Result` values instead of
|
|
487
|
+
throwing expected failures.
|
|
649
488
|
|
|
650
|
-
|
|
651
|
-
`map`, `mapErr`, `andThen`, or `orElse` when the value or error should change.
|
|
489
|
+
## Unexpected Errors
|
|
652
490
|
|
|
653
|
-
|
|
491
|
+
Expected application failures belong in `Err`. Bugs, impossible states, and unrecoverable conditions
|
|
492
|
+
should remain normal JavaScript throws or rejected promises.
|
|
654
493
|
|
|
655
|
-
|
|
656
|
-
import { ok } from 'resultar'
|
|
494
|
+
Use this boundary rule:
|
|
657
495
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
496
|
+
- Convert uncontrolled throws and rejections into expected errors with `tryResult`, `tryResultAsync`,
|
|
497
|
+
`fromThrowable`, `fromThrowableAsync`, or `fromPromise`.
|
|
498
|
+
- Keep your own recoverable domain failures as `Err`.
|
|
499
|
+
- Use `unwrapOrThrow()` only at final edges where turning an `Err` into a thrown error is deliberate.
|
|
661
500
|
|
|
662
|
-
|
|
663
|
-
|
|
501
|
+
```ts
|
|
502
|
+
const user = await fetchUser('usr_123').unwrapOrThrow()
|
|
664
503
|
```
|
|
665
504
|
|
|
666
|
-
`
|
|
505
|
+
Inside `safeTry`, the object form can map thrown bugs from the generator body into an explicit error:
|
|
667
506
|
|
|
668
507
|
```ts
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
const
|
|
672
|
-
|
|
508
|
+
const config = safeTry({
|
|
509
|
+
*try() {
|
|
510
|
+
const raw = yield* readConfigFile()
|
|
511
|
+
return ok(JSON.parse(raw) as Config)
|
|
512
|
+
},
|
|
513
|
+
catch: (cause) => new ParseConfigError({ cause }),
|
|
673
514
|
})
|
|
674
|
-
|
|
675
|
-
// still Result<never, Error>
|
|
676
|
-
result
|
|
677
515
|
```
|
|
678
516
|
|
|
679
|
-
|
|
680
|
-
|
|
517
|
+
## Linear Result Code
|
|
518
|
+
|
|
519
|
+
Use `safeTry` when a pipeline reads better as linear code. `yield*` unwraps the `Ok` value and
|
|
520
|
+
short-circuits on the first `Err`.
|
|
681
521
|
|
|
682
522
|
```ts
|
|
683
|
-
|
|
684
|
-
if (error) {
|
|
685
|
-
console.error('find user failed', error)
|
|
686
|
-
return
|
|
687
|
-
}
|
|
523
|
+
import { safeTry } from 'resultar'
|
|
688
524
|
|
|
689
|
-
|
|
690
|
-
|
|
525
|
+
const createUser = (
|
|
526
|
+
email: string,
|
|
527
|
+
): StrictResult<User, InvalidEmailError | UserAlreadyExistsError> =>
|
|
528
|
+
safeTry(function* () {
|
|
529
|
+
const validEmail = yield* validateEmail(email)
|
|
530
|
+
const availableEmail = yield* ensureUserDoesNotExist(validEmail)
|
|
531
|
+
|
|
532
|
+
return insertUser(availableEmail)
|
|
533
|
+
})
|
|
691
534
|
```
|
|
692
535
|
|
|
693
|
-
|
|
694
|
-
still preserved, even if the callback throws or rejects.
|
|
536
|
+
The same `safeTry` helper supports async generators:
|
|
695
537
|
|
|
696
538
|
```ts
|
|
697
|
-
const
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
539
|
+
const createUserAsync = (
|
|
540
|
+
email: string,
|
|
541
|
+
): StrictResultAsync<User, InvalidEmailError | UserAlreadyExistsError | DatabaseError> =>
|
|
542
|
+
safeTry(async function* () {
|
|
543
|
+
const validEmail = yield* validateEmail(email)
|
|
544
|
+
const availableEmail = yield* ensureUserDoesNotExist(validEmail)
|
|
545
|
+
|
|
546
|
+
return insertUserAsync(availableEmail)
|
|
547
|
+
})
|
|
701
548
|
```
|
|
702
549
|
|
|
703
|
-
|
|
704
|
-
until the resource is disposed with `using`, `await using`, or the matching symbol method.
|
|
550
|
+
The object form can map unexpected thrown errors while leaving yielded `Err` values unchanged:
|
|
705
551
|
|
|
706
552
|
```ts
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
553
|
+
const config = safeTry({
|
|
554
|
+
*try() {
|
|
555
|
+
const raw = yield* readConfigFile()
|
|
710
556
|
|
|
711
|
-
|
|
712
|
-
|
|
557
|
+
return ok(JSON.parse(raw) as Config)
|
|
558
|
+
},
|
|
559
|
+
catch: (cause) => new ParseConfigError({ cause }),
|
|
713
560
|
})
|
|
714
561
|
```
|
|
715
562
|
|
|
716
|
-
|
|
717
|
-
so observability or cleanup code does not replace the original result. For `ResultAsync`, rejected
|
|
718
|
-
callback promises are ignored too. Put fallible work in `andThen`, `orElse`, `tryCatch`, or
|
|
719
|
-
`tryCatchAsync` when those failures should become part of the result type.
|
|
563
|
+
## No-Discard Validation
|
|
720
564
|
|
|
721
|
-
|
|
565
|
+
Resultar values should not be ignored. Install `resultar-ls` to report discarded `Result` and
|
|
566
|
+
`ResultAsync` values.
|
|
722
567
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
Use `tryCatch` and `tryCatchAsync` only where uncontrolled code can throw. Convert that failure into
|
|
726
|
-
a typed error once, then return `Result` values from your own code.
|
|
727
|
-
|
|
728
|
-
```ts
|
|
729
|
-
const parseConfig = (input: string): StrictResult<Config, ParseConfigError> =>
|
|
730
|
-
tryCatch(
|
|
731
|
-
() => JSON.parse(input) as Config,
|
|
732
|
-
(cause) => new ParseConfigError({ cause }),
|
|
733
|
-
)
|
|
568
|
+
```sh
|
|
569
|
+
pnpm add -D resultar-ls typescript
|
|
734
570
|
```
|
|
735
571
|
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
Resultar keeps the wrapper model, but recommends tagged `Error` values for production error
|
|
739
|
-
channels. Use `.value` only after `isOk()` and `.error` only after `isErr()`, or prefer `match` /
|
|
740
|
-
`matchTags` at boundaries.
|
|
741
|
-
|
|
742
|
-
### From raw `T | Error`
|
|
743
|
-
|
|
744
|
-
Raw unions are concise, but they rely on a convention that successful values are never `Error`
|
|
745
|
-
instances. Resultar keeps success and failure structurally separate, so `Ok<Error>` is still a valid
|
|
746
|
-
success value when a domain needs it.
|
|
747
|
-
|
|
748
|
-
### From Effect
|
|
749
|
-
|
|
750
|
-
Resultar does not provide dependency injection, fibers, scopes, schedules, streams, or a runtime. It
|
|
751
|
-
borrows the useful discipline of typed expected errors and composable recovery while staying a small
|
|
752
|
-
library around `Result` and `ResultAsync`.
|
|
753
|
-
|
|
754
|
-
## API
|
|
572
|
+
Add a lint-like script:
|
|
755
573
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
- `safeTry(generator)`
|
|
764
|
-
- `StrictResult<T, E extends Error>` type alias for Error-only failure channels
|
|
765
|
-
|
|
766
|
-
### Result Methods
|
|
767
|
-
|
|
768
|
-
- `isOk()`
|
|
769
|
-
- `isErr()`
|
|
770
|
-
- `map(fn)`
|
|
771
|
-
- `mapErr(fn)`
|
|
772
|
-
- `filterOrElse(predicate, onFalse)`
|
|
773
|
-
- `andThen(fn)`
|
|
774
|
-
- `asyncAndThen(fn)`
|
|
775
|
-
- `orElse(fn)`
|
|
776
|
-
- `catchTag(tag, fn)`
|
|
777
|
-
- `catchTags(handlers)`
|
|
778
|
-
- `tap(fn)` run a side effect only when `Ok`; preserves the original result
|
|
779
|
-
- `tapError(fn)` run a side effect only when `Err`; preserves the original result
|
|
780
|
-
- `log(fn)` run a side effect for both states as `(value, error)`; preserves the original result
|
|
781
|
-
- `toDisposable(fn)` create a Node.js `Disposable` result for `using`
|
|
782
|
-
- `match(okFn, errFn)`
|
|
783
|
-
- `matchTags(okFn, handlers)`
|
|
784
|
-
- `matchTagsPartial(okFn, handlers, fallback)`
|
|
785
|
-
- `pipe(fn, ...)`
|
|
786
|
-
- `asyncMap(fn)`
|
|
787
|
-
- `unwrapOr(defaultValue)`
|
|
788
|
-
- `unwrapOrThrow()`
|
|
789
|
-
- `_unsafeUnwrap()`
|
|
790
|
-
- `_unsafeUnwrapErr()`
|
|
791
|
-
- `safeUnwrap()`
|
|
792
|
-
|
|
793
|
-
### Result Static Methods
|
|
794
|
-
|
|
795
|
-
- `Result.ok(value)`
|
|
796
|
-
- `Result.err(error)`
|
|
797
|
-
- `Result.unit()`
|
|
798
|
-
- `Result.tryCatch(fn, errorFn?)`
|
|
799
|
-
- `Result.fromThrowable(fn, errorFn?)`
|
|
800
|
-
- `Result.combine(results)`
|
|
801
|
-
- `Result.combineWithAllErrors(results)`
|
|
574
|
+
```json
|
|
575
|
+
{
|
|
576
|
+
"scripts": {
|
|
577
|
+
"lint:resultar": "resultar-no-discard --project tsconfig.json"
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
```
|
|
802
581
|
|
|
803
|
-
|
|
582
|
+
This fails:
|
|
804
583
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
- `fromPromise(promise, errorFn)`
|
|
809
|
-
- `fromSafePromise(promise)`
|
|
810
|
-
- `fromThrowableAsync(fn, errorFn?)`
|
|
811
|
-
- `tryCatchAsync(promiseOrFn, errorFn?)` catches rejections and synchronous throws from factories
|
|
812
|
-
- `StrictResultAsync<T, E extends Error>` type alias for Error-only async failure channels
|
|
584
|
+
```ts
|
|
585
|
+
saveUser(input)
|
|
586
|
+
```
|
|
813
587
|
|
|
814
|
-
|
|
588
|
+
These are intentional:
|
|
815
589
|
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
- `tapError(fn)` run a sync or async side effect only when `Err`; preserves the original result
|
|
822
|
-
- `log(fn)` run a sync or async side effect for both states as `(value, error)`
|
|
823
|
-
- `toAsyncDisposable(fn)` create a Node.js `AsyncDisposable` result for `await using`
|
|
824
|
-
- `match(okFn, errFn)`
|
|
825
|
-
- `matchTags(okFn, handlers)`
|
|
826
|
-
- `matchTagsPartial(okFn, handlers, fallback)`
|
|
827
|
-
- `unwrapOr(defaultValue)`
|
|
828
|
-
- `unwrapOrThrow()`
|
|
590
|
+
```ts
|
|
591
|
+
const result = saveUser(input)
|
|
592
|
+
return saveUser(input)
|
|
593
|
+
void saveUser(input)
|
|
594
|
+
```
|
|
829
595
|
|
|
830
|
-
|
|
596
|
+
For editor diagnostics, enable the TypeScript language-service plugin:
|
|
831
597
|
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
-
|
|
836
|
-
|
|
837
|
-
|
|
598
|
+
```json
|
|
599
|
+
{
|
|
600
|
+
"compilerOptions": {
|
|
601
|
+
"plugins": [{ "name": "resultar-ls", "noDiscard": "error" }]
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
```
|
|
838
605
|
|
|
839
|
-
|
|
606
|
+
TypeScript language-service plugins are editor-only by default. To make `tsc` report Resultar
|
|
607
|
+
diagnostics during builds, patch the local TypeScript installation:
|
|
840
608
|
|
|
841
|
-
|
|
609
|
+
```sh
|
|
610
|
+
pnpm exec resultar-ls patch
|
|
611
|
+
pnpm exec resultar-ls check
|
|
612
|
+
pnpm exec resultar-ls unpatch
|
|
613
|
+
```
|
|
842
614
|
|
|
843
|
-
|
|
844
|
-
|
|
615
|
+
For TypeScript 7 native-preview projects, `resultar-tsgo` exposes a `tsgo` wrapper that runs native
|
|
616
|
+
TypeScript and then Resultar no-discard validation.
|
|
845
617
|
|
|
846
|
-
|
|
847
|
-
```
|
|
618
|
+
## API Decision Guide
|
|
848
619
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
620
|
+
| Need | Use |
|
|
621
|
+
| --- | --- |
|
|
622
|
+
| Create a sync success | `ok(value)` |
|
|
623
|
+
| Create a sync failure | `err(error)` or `ErrorClass.err(props)` |
|
|
624
|
+
| Create an async success | `okAsync(value)` |
|
|
625
|
+
| Create an async failure | `errAsync(error)` |
|
|
626
|
+
| Create an `Ok(undefined)` | `unit()` or `unitAsync()` |
|
|
627
|
+
| Convert throwing sync code | `tryResult` or `fromThrowable` |
|
|
628
|
+
| Convert rejecting async code | `tryResultAsync`, `fromPromise`, or `fromThrowableAsync` |
|
|
629
|
+
| Treat an `Err` as unrecoverable at an edge | `unwrapOrThrow()` |
|
|
630
|
+
| Transform success | `map` or `asyncMap` |
|
|
631
|
+
| Replace success with a known value | `as(value)` |
|
|
632
|
+
| Transform error | `mapErr` |
|
|
633
|
+
| Continue with fallible work | `andThen` or `asyncAndThen` |
|
|
634
|
+
| Recover from failure | `orElse`, `catchTag`, or `catchTags` |
|
|
635
|
+
| Replace failure with another failure | `mapErr` or `orElse(() => err(newError))` |
|
|
636
|
+
| Replace failure with success | `orElse(() => ok(defaultValue))` |
|
|
637
|
+
| Handle a boundary | `match`, `matchTags`, or `matchTagsPartial` |
|
|
638
|
+
| Write linear result code | `safeTry` |
|
|
639
|
+
| Combine two independent results | `Result.zip` or `ResultAsync.zip` |
|
|
640
|
+
| Combine many independent results | `Result.combine` or `ResultAsync.combine` |
|
|
641
|
+
| Collect all validation failures | `validateAll` or `combineWithAllErrors` |
|
|
642
|
+
| Try fallback candidates | `firstSuccessOf` |
|
|
643
|
+
| Process collections sequentially | `loop`, `iterate`, or `forEach` |
|
|
644
|
+
| Observe without changing outcome | `tap`, `tapError`, or `log` |
|
|
645
|
+
| Throw intentionally at a final edge | `unwrapOrThrow()` |
|
|
646
|
+
| Default intentionally at a final edge | `unwrapOr(defaultValue)` |
|
|
647
|
+
|
|
648
|
+
## Version 3 Alpha Notes
|
|
649
|
+
|
|
650
|
+
`3.0.0-alpha.1` is a semver-major prerelease:
|
|
651
|
+
|
|
652
|
+
- `no-discard` tooling moved out of the runtime package and into dedicated packages.
|
|
653
|
+
- TypeScript peer support is now `>=6.0.0`.
|
|
654
|
+
- Deprecated APIs are not part of the current API:
|
|
655
|
+
|
|
656
|
+
- `Result#finally`
|
|
657
|
+
- `ResultAsync#finally`
|
|
658
|
+
- `ResultAsync#safeUnwrap`
|
|
659
|
+
- `safeTryAsync`
|
|
660
|
+
|
|
661
|
+
Use `log`, `toDisposable`, `toAsyncDisposable`, and `safeTry(async function* () { ... })` instead.
|
|
662
|
+
`Result#safeUnwrap()` remains available for compatibility, but new code should prefer
|
|
663
|
+
`yield* result`.
|
|
664
|
+
|
|
665
|
+
`tryCatch` and `tryCatchAsync` remain exported as backward-compatible aliases. Prefer `tryResult`
|
|
666
|
+
and `tryResultAsync` in new code and docs.
|
|
667
|
+
|
|
668
|
+
## Documentation
|
|
669
|
+
|
|
670
|
+
This README is the project entry point. For a full guide with larger examples and the complete API
|
|
671
|
+
map, see [DOCUMENTATION.md](DOCUMENTATION.md).
|
|
672
|
+
|
|
673
|
+
More focused material:
|
|
674
|
+
|
|
675
|
+
- [Type-safe error handling article, English](articles/en/type-safe.md)
|
|
676
|
+
- [Artigo sobre tratamento de erros type-safe, Portuguese](articles/pt/type-safe.md)
|
|
677
|
+
- [resultar-ls package guide](packages/resultar-ls/README.md)
|
|
678
|
+
- [resultar-tsgo package guide](packages/resultar-tsgo/README.md)
|
|
679
|
+
|
|
680
|
+
## Workspace
|
|
681
|
+
|
|
682
|
+
This repository is a pnpm workspace:
|
|
683
|
+
|
|
684
|
+
- `packages/resultar`: Resultar runtime package.
|
|
685
|
+
- `packages/resultar-ls`: TypeScript language-service diagnostics and no-discard CLI.
|
|
686
|
+
- `packages/resultar-tsgo`: TypeScript 7 native-preview `tsgo` wrapper plus Resultar no-discard
|
|
687
|
+
validation.
|
|
688
|
+
- `benchmarks`: benchmark package.
|
|
689
|
+
- `examples/language-service`: TypeScript 6 no-discard smoke example.
|
|
690
|
+
- `examples/tsgo`: TypeScript 7 native-preview smoke example.
|
|
691
|
+
|
|
692
|
+
Common commands:
|
|
858
693
|
|
|
859
694
|
```sh
|
|
860
695
|
pnpm install
|
|
@@ -864,14 +699,10 @@ pnpm test
|
|
|
864
699
|
pnpm run test:cov
|
|
865
700
|
pnpm run build
|
|
866
701
|
pnpm run check:full
|
|
702
|
+
pnpm run test:language-service
|
|
703
|
+
pnpm run bench
|
|
867
704
|
```
|
|
868
705
|
|
|
869
|
-
Package output:
|
|
870
|
-
|
|
871
|
-
- `dist/index.js`
|
|
872
|
-
- `dist/index.js.map`
|
|
873
|
-
- `dist/index.d.ts`
|
|
874
|
-
|
|
875
706
|
## License
|
|
876
707
|
|
|
877
708
|
MIT
|