resultar 3.5.0 → 3.5.1
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 +452 -862
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -1,52 +1,55 @@
|
|
|
1
1
|
# Resultar
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Typed, composable error handling for TypeScript.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
## New In 3.1
|
|
9
|
-
|
|
10
|
-
Resultar 3.1 turns common production async policies into typed values instead of rejected promises,
|
|
11
|
-
hidden timers, or helper code that throws.
|
|
12
|
-
|
|
13
|
-
- `ResultAsync.timeout`, `retry`, `retryOrElse`, `race`, `raceAll`, and `withResource` cover
|
|
14
|
-
timeouts, retry policy, fallback, replica reads, and cleanup while preserving the expected error
|
|
15
|
-
type.
|
|
16
|
-
- `ResultAsync.forEach` and `ResultAsync.validateAll` support sequential work, bounded
|
|
17
|
-
`{ concurrency }`, and explicit unbounded execution for batch processing and validation.
|
|
18
|
-
- `AbortError` / `isAbortError` and redacted tagged-error props with `redact`, `isRedacted`, and
|
|
19
|
-
`revealRedacted` make cancellation and sensitive error metadata visible at the type boundary.
|
|
20
|
-
- `pipe` lets teams package reusable result combinators without hiding the underlying `Result` /
|
|
21
|
-
`ResultAsync` type.
|
|
22
|
-
|
|
23
|
-
## Main Functionalities
|
|
24
|
-
|
|
25
|
-
Resultar gives you typed `Result` primitives, real `Error`-based tagged failures, async control
|
|
26
|
-
helpers, sync/async composition, and optional no-discard diagnostics. It is small enough to adopt in
|
|
27
|
-
one module and strict enough to use at service, HTTP, job, queue, CLI, and integration boundaries.
|
|
5
|
+
Resultar makes expected failures visible in function signatures instead of hiding them behind
|
|
6
|
+
`throw`, rejected promises, nullable values, or undocumented conventions.
|
|
28
7
|
|
|
29
8
|
```ts
|
|
30
9
|
Result<T, E>
|
|
31
10
|
ResultAsync<T, E>
|
|
32
|
-
StrictResult<T, E extends Error>
|
|
33
|
-
StrictResultAsync<T, E extends Error>
|
|
34
11
|
```
|
|
35
12
|
|
|
36
|
-
|
|
13
|
+
- `T` is the success value.
|
|
14
|
+
- `E` is the expected failure.
|
|
15
|
+
- Callers must handle the result before they can use the value.
|
|
37
16
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
JSON output.
|
|
41
|
-
- `ResultAsync` can bound concurrent work, race replicas, apply typed timeouts, retry transient
|
|
42
|
-
failures, and pair acquisition with cleanup without adding a runtime scheduler.
|
|
43
|
-
- Sync and async fallible work compose without `try/catch` blocks scattered through application
|
|
44
|
-
code.
|
|
45
|
-
- Ignored `Result` values can be reported by a type-aware no-discard check.
|
|
17
|
+
Resultar stays focused on explicit values. It does not require a framework, dependency injection
|
|
18
|
+
container, scheduler, or application runtime.
|
|
46
19
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
20
|
+
## Highlights
|
|
21
|
+
|
|
22
|
+
| You need | Resultar gives you |
|
|
23
|
+
| --- | --- |
|
|
24
|
+
| Expected failures in the type system | `Result<T, E>` and `ResultAsync<T, E>` |
|
|
25
|
+
| Production errors with useful metadata | Real `Error` subclasses from `createTaggedError` |
|
|
26
|
+
| Composable sync and async workflows | `map`, `mapErr`, `andThen`, `asyncAndThen`, and `orElse` |
|
|
27
|
+
| Linear code without scattered `try/catch` | `safeTry` with `yield*` short-circuiting |
|
|
28
|
+
| Explicit production async policy | Typed timeout, retry, race, concurrency, and cleanup helpers |
|
|
29
|
+
| Exhaustive application boundaries | `matchTags` for tagged error unions |
|
|
30
|
+
| Guardrails for ignored results | The optional `resultar-check` CLI and language-server plugin |
|
|
31
|
+
|
|
32
|
+
Resultar began as a fork of `neverthrow`. The current API keeps the familiar explicit Result model
|
|
33
|
+
and adds Resultar-specific tagged errors, production async helpers, exhaustive tagged boundaries,
|
|
34
|
+
and optional TypeScript-backed diagnostics.
|
|
35
|
+
|
|
36
|
+
## Contents
|
|
37
|
+
|
|
38
|
+
- [Install](#install)
|
|
39
|
+
- [Why Resultar](#why-resultar)
|
|
40
|
+
- [Quick Start](#quick-start)
|
|
41
|
+
- [Compose Results](#compose-results)
|
|
42
|
+
- [Tagged Errors](#tagged-errors)
|
|
43
|
+
- [Async Work](#async-work)
|
|
44
|
+
- [Production Async Policies](#production-async-policies)
|
|
45
|
+
- [Linear Workflows With safeTry](#linear-workflows-with-safetry)
|
|
46
|
+
- [Recover Inside A Workflow](#recover-inside-a-workflow)
|
|
47
|
+
- [Handle Results At Boundaries](#handle-results-at-boundaries)
|
|
48
|
+
- [Strict Results At Production Boundaries](#strict-results-at-production-boundaries)
|
|
49
|
+
- [Prevent Ignored Results](#prevent-ignored-results)
|
|
50
|
+
- [HTTP Request Packages](#http-request-packages)
|
|
51
|
+
- [API Decision Guide](#api-decision-guide)
|
|
52
|
+
- [More Documentation](#more-documentation)
|
|
50
53
|
|
|
51
54
|
## Install
|
|
52
55
|
|
|
@@ -58,943 +61,592 @@ pnpm add resultar
|
|
|
58
61
|
npm install resultar
|
|
59
62
|
```
|
|
60
63
|
|
|
61
|
-
|
|
64
|
+
Requirements:
|
|
62
65
|
|
|
63
66
|
- Node.js 24+
|
|
64
|
-
- TypeScript >=7 for the canonical Resultar diagnostics workflow
|
|
65
67
|
- ESM only
|
|
68
|
+
- TypeScript 7+ for the canonical `resultar-check` diagnostics workflow
|
|
66
69
|
|
|
67
70
|
```ts
|
|
68
71
|
import { createTaggedError, ok } from 'resultar'
|
|
69
|
-
import type {
|
|
72
|
+
import type { Result } from 'resultar'
|
|
70
73
|
```
|
|
71
74
|
|
|
72
75
|
CommonJS `require('resultar')` is not exported.
|
|
73
76
|
|
|
74
|
-
## Why
|
|
77
|
+
## Why Resultar
|
|
75
78
|
|
|
76
|
-
This signature
|
|
79
|
+
This signature does not tell callers that parsing can fail:
|
|
77
80
|
|
|
78
81
|
```ts
|
|
79
|
-
const parsePort = (
|
|
80
|
-
const port = Number(
|
|
82
|
+
const parsePort = (input: string): number => {
|
|
83
|
+
const port = Number(input)
|
|
81
84
|
|
|
82
85
|
if (!Number.isInteger(port) || port <= 0) {
|
|
83
|
-
throw new Error(`Invalid port ${
|
|
86
|
+
throw new Error(`Invalid port ${input}`)
|
|
84
87
|
}
|
|
85
88
|
|
|
86
89
|
return port
|
|
87
90
|
}
|
|
88
91
|
```
|
|
89
92
|
|
|
90
|
-
|
|
93
|
+
Resultar moves the expected failure into the contract:
|
|
91
94
|
|
|
92
95
|
```ts
|
|
93
96
|
import { createTaggedError, ok } from 'resultar'
|
|
94
|
-
import type {
|
|
97
|
+
import type { Result } from 'resultar'
|
|
95
98
|
|
|
96
99
|
class InvalidPortError extends createTaggedError({
|
|
97
100
|
name: 'InvalidPortError',
|
|
98
|
-
message: 'Invalid port $
|
|
101
|
+
message: 'Invalid port $input',
|
|
99
102
|
}) {}
|
|
100
103
|
|
|
101
|
-
const parsePort = (
|
|
102
|
-
const port = Number(
|
|
104
|
+
const parsePort = (input: string): Result<number, InvalidPortError> => {
|
|
105
|
+
const port = Number(input)
|
|
103
106
|
|
|
104
|
-
return Number.isInteger(port) && port > 0 ? ok(port) : InvalidPortError.err({
|
|
107
|
+
return Number.isInteger(port) && port > 0 ? ok(port) : InvalidPortError.err({ input })
|
|
105
108
|
}
|
|
106
109
|
```
|
|
107
110
|
|
|
108
|
-
|
|
109
|
-
That is the main trade: expected failures become visible contracts instead of tribal knowledge.
|
|
110
|
-
|
|
111
|
-
Resultar is intentionally not an application runtime. It gives you explicit error values that work
|
|
112
|
-
in normal TypeScript.
|
|
113
|
-
|
|
114
|
-
## What You Get
|
|
115
|
-
|
|
116
|
-
| Problem | Resultar gives you |
|
|
117
|
-
| --- | --- |
|
|
118
|
-
| Expected failures are hidden behind `throw` | `Result<T, E>` and `ResultAsync<T, E>` in function signatures |
|
|
119
|
-
| Production errors need stable names and metadata | `createTaggedError` classes with `_tag`, template props, `cause`, and `.toJSON()` |
|
|
120
|
-
| Async flows turn into nested `try/catch` | `tryResultAsync`, `andThen`, `asyncAndThen`, `map`, `orElse`, `safeTry` |
|
|
121
|
-
| Repeated result transforms become noisy | `pipe` with small reusable combinators |
|
|
122
|
-
| Network calls need timeouts, retries, and fallback policy | `ResultAsync.timeout`, `retry`, `retryOrElse`, `race`, and `raceAll` |
|
|
123
|
-
| Batch work needs backpressure without a framework | `ResultAsync.forEach` and mapped `validateAll` with `{ concurrency }` |
|
|
124
|
-
| Resourceful work needs cleanup on every path | `ResultAsync.withResource` and native `AsyncIterable<Result<T, E>>` recipes |
|
|
125
|
-
| Boundary handlers miss cases | `matchTags` for exhaustive tagged-error handling |
|
|
126
|
-
| Results are easy to ignore | `resultar-check` and the `resultar-check` TypeScript plugin |
|
|
127
|
-
| You do not want another application runtime | A small ESM library around explicit values |
|
|
128
|
-
|
|
129
|
-
Nested `try/catch` often masks the error the caller actually needs:
|
|
111
|
+
The caller can now see both outcomes without reading the implementation:
|
|
130
112
|
|
|
131
113
|
```ts
|
|
132
|
-
const
|
|
133
|
-
try {
|
|
134
|
-
const response = await fetch(url)
|
|
135
|
-
|
|
136
|
-
if (response.status >= 400) {
|
|
137
|
-
throw new HttpClientError(response.status)
|
|
138
|
-
}
|
|
114
|
+
const port = parsePort(process.env.PORT ?? '3000')
|
|
139
115
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
Resultar keeps each failure explicit and typed:
|
|
148
|
-
|
|
149
|
-
```ts
|
|
150
|
-
const getPosts = (
|
|
151
|
-
url: string,
|
|
152
|
-
): StrictResultAsync<Post[], FetchPostsError | HttpClientError | ParsePostsError> =>
|
|
153
|
-
tryResultAsync(fetch(url), (cause) => new FetchPostsError({ cause }))
|
|
154
|
-
.andThen((response) =>
|
|
155
|
-
response.status >= 400 ? HttpClientError.err({ status: response.status }) : ok(response),
|
|
156
|
-
)
|
|
157
|
-
.andThen((response) =>
|
|
158
|
-
tryResultAsync(response.json() as Promise<Post[]>, (cause) => new ParsePostsError({ cause })),
|
|
159
|
-
)
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
The same flow can be written linearly with `safeTry` when the chain gets harder to scan:
|
|
163
|
-
|
|
164
|
-
```ts
|
|
165
|
-
const getPostsLinear = (
|
|
166
|
-
url: string,
|
|
167
|
-
): StrictResultAsync<Post[], FetchPostsError | HttpClientError | ParsePostsError> =>
|
|
168
|
-
safeTry(async function* () {
|
|
169
|
-
const response = yield* tryResultAsync(
|
|
170
|
-
fetch(url),
|
|
171
|
-
(cause) => new FetchPostsError({ cause }),
|
|
172
|
-
)
|
|
173
|
-
|
|
174
|
-
if (response.status >= 400) {
|
|
175
|
-
return HttpClientError.err({ status: response.status })
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const posts = yield* tryResultAsync(
|
|
179
|
-
response.json() as Promise<Post[]>,
|
|
180
|
-
(cause) => new ParsePostsError({ cause }),
|
|
181
|
-
)
|
|
182
|
-
|
|
183
|
-
return ok(posts)
|
|
184
|
-
})
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
## Async Control Without A Runtime
|
|
188
|
-
|
|
189
|
-
The 3.1 async helpers cover common production workflows while keeping plain `Promise` and
|
|
190
|
-
`AbortSignal`-compatible APIs underneath. You choose the policy in the type signature instead of
|
|
191
|
-
hiding it in a helper that throws.
|
|
192
|
-
|
|
193
|
-
Retry transient work and fall back to cache only after the retry policy is exhausted:
|
|
194
|
-
|
|
195
|
-
```ts
|
|
196
|
-
const user = ResultAsync.retryOrElse(
|
|
197
|
-
(attempt, signal) => fetchUser(id, { attempt, signal }),
|
|
198
|
-
{
|
|
199
|
-
times: 2,
|
|
200
|
-
delayMs: ({ nextAttempt }) => nextAttempt * 100,
|
|
201
|
-
jittered: 0.5,
|
|
202
|
-
while: (error) => error._tag === 'RateLimitError' || error._tag === 'ServiceUnavailableError',
|
|
203
|
-
orElse: () => readCachedUser(id),
|
|
204
|
-
},
|
|
205
|
-
)
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
`jittered` randomizes the computed retry delay by a percentage factor to avoid synchronized retries.
|
|
209
|
-
For example, `delayMs: 100` with `jittered: 0.5` waits between `50ms` and `150ms`; omit it or use
|
|
210
|
-
`0` to keep the exact delay.
|
|
211
|
-
|
|
212
|
-
Race a primary and replica with cooperative loser abort:
|
|
213
|
-
|
|
214
|
-
```ts
|
|
215
|
-
const user = ResultAsync.race(
|
|
216
|
-
(signal) => readUserFromPrimary(id, { signal }),
|
|
217
|
-
(signal) => readUserFromReplica(id, { signal }),
|
|
116
|
+
const message = port.match(
|
|
117
|
+
(value) => `Listening on ${value}`,
|
|
118
|
+
(error) => error.message,
|
|
218
119
|
)
|
|
219
120
|
```
|
|
220
121
|
|
|
221
|
-
|
|
122
|
+
Use `Err` for failures the caller can reasonably handle: invalid input, conflicts, missing records,
|
|
123
|
+
rate limits, timeouts, and unavailable dependencies. Keep programming bugs and impossible states as
|
|
124
|
+
normal JavaScript throws.
|
|
222
125
|
|
|
223
|
-
|
|
224
|
-
const user = ResultAsync.timeout(
|
|
225
|
-
(signal) => readUserFromPrimary(id, { signal }),
|
|
226
|
-
{
|
|
227
|
-
timeoutMs: 1_500,
|
|
228
|
-
onTimeout: () => new FetchUserTimeoutError({ id, timeoutMs: 1_500 }),
|
|
229
|
-
},
|
|
230
|
-
)
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
Process batches with bounded concurrency and explicit cleanup:
|
|
126
|
+
## Quick Start
|
|
234
127
|
|
|
235
|
-
|
|
236
|
-
const imported = ResultAsync.withResource({
|
|
237
|
-
acquire: (signal) => openImportSession({ signal }),
|
|
238
|
-
use: (session, signal) =>
|
|
239
|
-
ResultAsync.forEach(
|
|
240
|
-
rows,
|
|
241
|
-
(row) => importRow(session, row, { signal }),
|
|
242
|
-
{ concurrency: 8, discard: true },
|
|
243
|
-
),
|
|
244
|
-
release: (session) => session.close(),
|
|
245
|
-
})
|
|
246
|
-
```
|
|
128
|
+
The examples below build one account-creation workflow.
|
|
247
129
|
|
|
248
|
-
|
|
130
|
+
Start with real tagged errors. Variables in the message template become required constructor props:
|
|
249
131
|
|
|
250
132
|
```ts
|
|
251
133
|
import { createTaggedError, ok } from 'resultar'
|
|
252
|
-
import type {
|
|
134
|
+
import type { Result } from 'resultar'
|
|
253
135
|
|
|
254
136
|
class InvalidEmailError extends createTaggedError({
|
|
255
137
|
name: 'InvalidEmailError',
|
|
256
138
|
message: 'Invalid email $email',
|
|
257
139
|
}) {}
|
|
258
140
|
|
|
259
|
-
class
|
|
260
|
-
name: '
|
|
261
|
-
message: '
|
|
141
|
+
class AccountAlreadyExistsError extends createTaggedError({
|
|
142
|
+
name: 'AccountAlreadyExistsError',
|
|
143
|
+
message: 'Account $email already exists',
|
|
262
144
|
}) {}
|
|
263
145
|
|
|
264
|
-
type
|
|
146
|
+
type Account = {
|
|
265
147
|
readonly email: string
|
|
266
148
|
readonly id: string
|
|
267
149
|
}
|
|
150
|
+
```
|
|
268
151
|
|
|
269
|
-
|
|
270
|
-
email.includes('@') ? ok(email) : InvalidEmailError.err({ email })
|
|
152
|
+
Return `Ok` for success and `Err` for an expected failure:
|
|
271
153
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
email === 'taken@example.com' ? UserAlreadyExistsError.err({ email }) : ok(email)
|
|
154
|
+
```ts
|
|
155
|
+
const normalizeEmail = (input: string): Result<string, InvalidEmailError> => {
|
|
156
|
+
const email = input.trim().toLowerCase()
|
|
276
157
|
|
|
277
|
-
|
|
278
|
-
|
|
158
|
+
return email.includes('@') ? ok(email) : InvalidEmailError.err({ email })
|
|
159
|
+
}
|
|
279
160
|
|
|
280
|
-
const
|
|
161
|
+
const ensureAccountIsAvailable = (
|
|
281
162
|
email: string,
|
|
282
|
-
):
|
|
283
|
-
|
|
163
|
+
): Result<string, AccountAlreadyExistsError> =>
|
|
164
|
+
email === 'taken@example.com' ? AccountAlreadyExistsError.err({ email }) : ok(email)
|
|
284
165
|
|
|
285
|
-
const
|
|
286
|
-
(
|
|
287
|
-
{
|
|
288
|
-
InvalidEmailError: (error) => ({
|
|
289
|
-
body: { code: error._tag, message: error.message },
|
|
290
|
-
statusCode: 400,
|
|
291
|
-
}),
|
|
292
|
-
UserAlreadyExistsError: (error) => ({
|
|
293
|
-
body: { code: error._tag, message: error.message },
|
|
294
|
-
statusCode: 409,
|
|
295
|
-
}),
|
|
296
|
-
},
|
|
297
|
-
)
|
|
166
|
+
const buildAccount = (email: string): Result<Account, never> =>
|
|
167
|
+
ok({ email, id: 'acct_123' })
|
|
298
168
|
```
|
|
299
169
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
`Result<T, E>` is a synchronous fallible value. It is either an `Ok<T>` or an `Err<E>`.
|
|
170
|
+
Compose the steps with `andThen`. The resulting error type is inferred as a union:
|
|
303
171
|
|
|
304
172
|
```ts
|
|
305
|
-
|
|
173
|
+
type CreateAccountError = InvalidEmailError | AccountAlreadyExistsError
|
|
306
174
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
if (result.isErr()) {
|
|
312
|
-
result.error
|
|
313
|
-
}
|
|
175
|
+
const createAccount = (input: string): Result<Account, CreateAccountError> =>
|
|
176
|
+
normalizeEmail(input).andThen(ensureAccountIsAvailable).andThen(buildAccount)
|
|
314
177
|
```
|
|
315
178
|
|
|
316
|
-
|
|
317
|
-
same composition API.
|
|
179
|
+
Inspect a result directly when local branching is clearer:
|
|
318
180
|
|
|
319
181
|
```ts
|
|
320
|
-
|
|
321
|
-
import type { StrictResult, StrictResultAsync } from 'resultar'
|
|
182
|
+
const account = createAccount('person@example.com')
|
|
322
183
|
|
|
323
|
-
|
|
324
|
-
|
|
184
|
+
if (account.isOk()) {
|
|
185
|
+
account.value.email
|
|
325
186
|
}
|
|
326
187
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
message: 'Failed to fetch user $id',
|
|
330
|
-
}) {}
|
|
331
|
-
|
|
332
|
-
class UserResponseError extends createTaggedError({
|
|
333
|
-
name: 'UserResponseError',
|
|
334
|
-
message: 'User request $id failed with status $status',
|
|
335
|
-
}) {}
|
|
336
|
-
|
|
337
|
-
class ParseUserError extends createTaggedError({
|
|
338
|
-
name: 'ParseUserError',
|
|
339
|
-
message: 'Failed to parse user $id response',
|
|
340
|
-
}) {}
|
|
341
|
-
|
|
342
|
-
class DecodeUserError extends createTaggedError({
|
|
343
|
-
name: 'DecodeUserError',
|
|
344
|
-
message: 'Invalid user $id response payload',
|
|
345
|
-
}) {}
|
|
346
|
-
|
|
347
|
-
const decodeUser = (input: unknown, id: string): StrictResult<User, DecodeUserError> => {
|
|
348
|
-
if (
|
|
349
|
-
typeof input === 'object' &&
|
|
350
|
-
input !== null &&
|
|
351
|
-
'id' in input &&
|
|
352
|
-
typeof input.id === 'string'
|
|
353
|
-
) {
|
|
354
|
-
return ok({ id: input.id })
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
return DecodeUserError.err({ id })
|
|
188
|
+
if (account.isErr()) {
|
|
189
|
+
account.error.message
|
|
358
190
|
}
|
|
359
|
-
|
|
360
|
-
const fetchUser = (
|
|
361
|
-
id: string,
|
|
362
|
-
): StrictResultAsync<User, FetchUserError | UserResponseError | ParseUserError | DecodeUserError> =>
|
|
363
|
-
fromPromise(fetch(`https://example.com/users/${id}`), (cause) => new FetchUserError({ cause, id }))
|
|
364
|
-
.andThen((response) =>
|
|
365
|
-
response.ok ? ok(response) : UserResponseError.err({ id, status: response.status }),
|
|
366
|
-
)
|
|
367
|
-
.andThen((response) =>
|
|
368
|
-
tryResultAsync<unknown, ParseUserError>(
|
|
369
|
-
() => response.json(),
|
|
370
|
-
(cause) => new ParseUserError({ cause, id }),
|
|
371
|
-
),
|
|
372
|
-
)
|
|
373
|
-
.andThen((input) => decodeUser(input, id))
|
|
374
|
-
|
|
375
|
-
const label = await fetchUser('usr_123').match(
|
|
376
|
-
(user) => `User ${user.id}`,
|
|
377
|
-
(error) => error.message,
|
|
378
|
-
)
|
|
379
191
|
```
|
|
380
192
|
|
|
381
|
-
|
|
382
|
-
separate. In production, treat parsing bytes into `unknown` and decoding `unknown` into a domain
|
|
383
|
-
type as different failure modes.
|
|
384
|
-
|
|
385
|
-
Use generic `Result<T, E>` for local flows where strings, enums, or small domain objects are useful
|
|
386
|
-
as `E`. Prefer `StrictResult<T, E extends Error>` and
|
|
387
|
-
`StrictResultAsync<T, E extends Error>` at service, HTTP, job, queue, CLI, and integration
|
|
388
|
-
boundaries.
|
|
389
|
-
|
|
390
|
-
## Tagged Errors
|
|
391
|
-
|
|
392
|
-
`createTaggedError` creates real `Error` subclasses with stable tags and typed message-template
|
|
393
|
-
props.
|
|
193
|
+
Or consume both outcomes with `match`:
|
|
394
194
|
|
|
395
195
|
```ts
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
message: 'User $id not found in $source',
|
|
401
|
-
}) {}
|
|
402
|
-
|
|
403
|
-
const error = new UserNotFoundError({
|
|
404
|
-
id: 'usr_123',
|
|
405
|
-
source: 'database',
|
|
406
|
-
})
|
|
407
|
-
|
|
408
|
-
error instanceof Error // true
|
|
409
|
-
error instanceof UserNotFoundError // true
|
|
410
|
-
error._tag // 'UserNotFoundError'
|
|
411
|
-
error.message // 'User usr_123 not found in database'
|
|
412
|
-
error.id // string | number
|
|
413
|
-
error.source // string | number
|
|
196
|
+
const label = createAccount('person@example.com').match(
|
|
197
|
+
(created) => `Created ${created.id}`,
|
|
198
|
+
(error) => `Could not create account: ${error.message}`,
|
|
199
|
+
)
|
|
414
200
|
```
|
|
415
201
|
|
|
416
|
-
|
|
202
|
+
## Compose Results
|
|
417
203
|
|
|
418
|
-
|
|
419
|
-
- `messageTemplate`
|
|
420
|
-
- `fingerprint`
|
|
421
|
-
- `cause`
|
|
422
|
-
- `.toJSON()`
|
|
423
|
-
- `.findCause(ErrorClass)`
|
|
424
|
-
- static `.is(value)` for nominal checks
|
|
425
|
-
- static `.err(props)` for returning `Err`
|
|
204
|
+
Resultar composition keeps success and failure channels separate.
|
|
426
205
|
|
|
427
|
-
|
|
428
|
-
errors.
|
|
206
|
+
### Transform success with `map`
|
|
429
207
|
|
|
430
208
|
```ts
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
// @ts-expect-error source is required
|
|
434
|
-
new UserNotFoundError({ id: 'usr_123' })
|
|
209
|
+
const accountId = createAccount('person@example.com').map((account) => account.id)
|
|
435
210
|
```
|
|
436
211
|
|
|
437
|
-
|
|
212
|
+
### Add a fallible condition with `filterOrElse`
|
|
438
213
|
|
|
439
214
|
```ts
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
CardDeclined: { readonly code: string }
|
|
445
|
-
InsufficientFunds: { readonly balance: number }
|
|
446
|
-
}>
|
|
447
|
-
|
|
448
|
-
const PaymentError = taggedEnum<{
|
|
449
|
-
CardDeclined: { readonly code: string }
|
|
450
|
-
InsufficientFunds: { readonly balance: number }
|
|
451
|
-
}>()
|
|
452
|
-
|
|
453
|
-
const declined = PaymentError.CardDeclined({ code: 'card_declined' })
|
|
215
|
+
class UnsupportedEmailDomainError extends createTaggedError({
|
|
216
|
+
name: 'UnsupportedEmailDomainError',
|
|
217
|
+
message: 'Email domain $domain is not supported',
|
|
218
|
+
}) {}
|
|
454
219
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
220
|
+
const requireCompanyEmail = (
|
|
221
|
+
email: string,
|
|
222
|
+
): Result<string, InvalidEmailError | UnsupportedEmailDomainError> =>
|
|
223
|
+
normalizeEmail(email).filterOrElse(
|
|
224
|
+
(normalized) => normalized.endsWith('@company.com'),
|
|
225
|
+
(normalized) =>
|
|
226
|
+
new UnsupportedEmailDomainError({ domain: normalized.split('@')[1] ?? 'unknown' }),
|
|
227
|
+
)
|
|
460
228
|
```
|
|
461
229
|
|
|
462
|
-
|
|
463
|
-
Use `revealRedacted(value)` only at the exact boundary that is allowed to see the secret.
|
|
464
|
-
|
|
465
|
-
## Composing Results
|
|
466
|
-
|
|
467
|
-
Use `map` for infallible transforms:
|
|
230
|
+
### Continue with fallible work using `andThen`
|
|
468
231
|
|
|
469
232
|
```ts
|
|
470
|
-
const
|
|
233
|
+
const createCompanyAccount = (input: string) =>
|
|
234
|
+
requireCompanyEmail(input).andThen(ensureAccountIsAvailable).andThen(buildAccount)
|
|
471
235
|
```
|
|
472
236
|
|
|
473
|
-
|
|
237
|
+
### Transform a failure with `mapErr`
|
|
474
238
|
|
|
475
239
|
```ts
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
240
|
+
class AccountServiceError extends createTaggedError({
|
|
241
|
+
name: 'AccountServiceError',
|
|
242
|
+
message: 'Account service failed for $email',
|
|
243
|
+
}) {}
|
|
480
244
|
|
|
481
|
-
|
|
482
|
-
|
|
245
|
+
const account = createAccount(input).mapErr(
|
|
246
|
+
(cause) => new AccountServiceError({ cause, email: input }),
|
|
247
|
+
)
|
|
483
248
|
```
|
|
484
249
|
|
|
485
|
-
|
|
250
|
+
### Recover with `orElse`
|
|
486
251
|
|
|
487
252
|
```ts
|
|
488
|
-
const
|
|
253
|
+
const account = findAccountByEmail(email).orElse(() => createAccount(email))
|
|
489
254
|
```
|
|
490
255
|
|
|
491
|
-
Use `
|
|
256
|
+
Use `pipe` when a result transform should be named and reused:
|
|
492
257
|
|
|
493
258
|
```ts
|
|
494
|
-
|
|
495
|
-
name: 'InvalidDomainError',
|
|
496
|
-
message: 'Invalid email domain $domain',
|
|
497
|
-
}) {}
|
|
498
|
-
|
|
499
|
-
const validateCompanyEmail = (
|
|
500
|
-
email: string,
|
|
501
|
-
): StrictResult<string, InvalidEmailError | InvalidDomainError> =>
|
|
502
|
-
validateEmail(email).filterOrElse(
|
|
503
|
-
(validEmail) => validEmail.endsWith('@company.com'),
|
|
504
|
-
(validEmail) => new InvalidDomainError({ domain: validEmail.split('@')[1] ?? 'unknown' }),
|
|
505
|
-
)
|
|
506
|
-
```
|
|
259
|
+
import type { Result } from 'resultar'
|
|
507
260
|
|
|
508
|
-
|
|
509
|
-
|
|
261
|
+
const auditAccount = <E>(result: Result<Account, E>): Result<Account, E> =>
|
|
262
|
+
result.tap((account) => logger.info({ accountId: account.id }, 'account ready'))
|
|
510
263
|
|
|
511
|
-
|
|
512
|
-
|
|
264
|
+
const accountEmail = createAccount(input)
|
|
265
|
+
.pipe(auditAccount)
|
|
266
|
+
.map((account) => account.email)
|
|
267
|
+
```
|
|
513
268
|
|
|
514
|
-
|
|
515
|
-
|
|
269
|
+
Callbacks passed to transform methods are normal application code. Resultar does not silently catch
|
|
270
|
+
exceptions thrown by `map`, `andThen`, or `pipe` callbacks. Wrap uncontrolled code at the edge
|
|
271
|
+
instead.
|
|
516
272
|
|
|
517
|
-
|
|
518
|
-
result: Result<string, InvalidEmailError>,
|
|
519
|
-
): StrictResult<string, InvalidEmailError | InvalidDomainError> =>
|
|
520
|
-
result.filterOrElse(
|
|
521
|
-
(email) => email.endsWith('@company.com'),
|
|
522
|
-
(email) => new InvalidDomainError({ domain: email.split('@')[1] ?? 'unknown' }),
|
|
523
|
-
)
|
|
524
|
-
|
|
525
|
-
const email = validateEmail(input).pipe(normalizeEmail, requireCompanyDomain)
|
|
526
|
-
```
|
|
273
|
+
## Tagged Errors
|
|
527
274
|
|
|
528
|
-
|
|
529
|
-
errors from the pipe callbacks:
|
|
275
|
+
`createTaggedError` produces real `Error` subclasses with stable tags and structured metadata:
|
|
530
276
|
|
|
531
277
|
```ts
|
|
532
|
-
|
|
278
|
+
const error = new AccountAlreadyExistsError({ email: 'person@example.com' })
|
|
533
279
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
.pipe(auditUser, (result) => result.map((user) => user.email))
|
|
280
|
+
error instanceof Error // true
|
|
281
|
+
error instanceof AccountAlreadyExistsError // true
|
|
282
|
+
error._tag // 'AccountAlreadyExistsError'
|
|
283
|
+
error.message // 'Account person@example.com already exists'
|
|
284
|
+
error.email // string | number
|
|
285
|
+
error.fingerprint // stable error fingerprint
|
|
286
|
+
error.toJSON() // serializable metadata
|
|
542
287
|
```
|
|
543
288
|
|
|
544
|
-
|
|
289
|
+
Tagged errors support:
|
|
545
290
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
)
|
|
553
|
-
|
|
291
|
+
- `_tag` for exhaustive handling;
|
|
292
|
+
- typed message-template props;
|
|
293
|
+
- `cause` and stack traces;
|
|
294
|
+
- `fingerprint` and `messageTemplate`;
|
|
295
|
+
- cycle-safe `.toJSON()` output;
|
|
296
|
+
- `.findCause(ErrorClass)` for nested causes;
|
|
297
|
+
- static `.is(value)` for nominal checks;
|
|
298
|
+
- static `.err(props)` for returning an `Err` directly.
|
|
554
299
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
Use `catchReason`, `catchReasons`, and `unwrapReason` when a stable parent error has a nested tagged
|
|
558
|
-
reason. This keeps the outer error useful for logs and boundaries while allowing local recovery by
|
|
559
|
-
the precise reason.
|
|
300
|
+
Preserve the original failure as `cause` when translating infrastructure errors:
|
|
560
301
|
|
|
561
302
|
```ts
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
QuotaExceededError: { readonly limit: number }
|
|
567
|
-
RateLimitError: { readonly retryAfterMs: number }
|
|
568
|
-
}>()
|
|
569
|
-
|
|
570
|
-
type AiReason = TaggedEnum<{
|
|
571
|
-
QuotaExceededError: { readonly limit: number }
|
|
572
|
-
RateLimitError: { readonly retryAfterMs: number }
|
|
573
|
-
}>
|
|
574
|
-
|
|
575
|
-
class AiError extends createTaggedError({
|
|
576
|
-
name: 'AiError',
|
|
577
|
-
message: 'AI request failed',
|
|
578
|
-
}) {
|
|
579
|
-
public readonly reason: AiReason
|
|
580
|
-
|
|
581
|
-
public constructor(props: { readonly cause?: unknown; readonly reason: AiReason }) {
|
|
582
|
-
super({ cause: props.cause })
|
|
583
|
-
this.reason = props.reason
|
|
584
|
-
}
|
|
585
|
-
}
|
|
303
|
+
class DatabaseError extends createTaggedError({
|
|
304
|
+
name: 'DatabaseError',
|
|
305
|
+
message: 'Database $operation failed',
|
|
306
|
+
}) {}
|
|
586
307
|
|
|
587
|
-
const
|
|
588
|
-
|
|
308
|
+
const databaseError = new DatabaseError({
|
|
309
|
+
cause: new Error('connection refused'),
|
|
310
|
+
operation: 'insert-account',
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
databaseError.findCause(Error)
|
|
589
314
|
```
|
|
590
315
|
|
|
316
|
+
Use `redact` when metadata must remain available to authorized code without leaking through error
|
|
317
|
+
messages or JSON:
|
|
318
|
+
|
|
591
319
|
```ts
|
|
592
|
-
|
|
593
|
-
ok(`retry after ${reason.retryAfterMs}ms`),
|
|
594
|
-
)
|
|
320
|
+
import { createTaggedError, isRedacted, redact, revealRedacted } from 'resultar'
|
|
595
321
|
|
|
596
|
-
|
|
597
|
-
|
|
322
|
+
class TokenRejectedError extends createTaggedError({
|
|
323
|
+
name: 'TokenRejectedError',
|
|
324
|
+
message: 'Token $token was rejected',
|
|
325
|
+
}) {}
|
|
598
326
|
|
|
599
|
-
|
|
600
|
-
|
|
327
|
+
const error = new TokenRejectedError({
|
|
328
|
+
token: redact('secret-token', 'api-token'),
|
|
329
|
+
})
|
|
601
330
|
|
|
602
|
-
|
|
603
|
-
log entry, queue acknowledgement, CLI exit code, or UI state.
|
|
331
|
+
error.message // 'Token <redacted:api-token> was rejected'
|
|
604
332
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
{
|
|
609
|
-
InvalidEmailError: (error) => ({
|
|
610
|
-
body: { code: error._tag, message: error.message },
|
|
611
|
-
statusCode: 400,
|
|
612
|
-
}),
|
|
613
|
-
},
|
|
614
|
-
(error) => ({
|
|
615
|
-
body: { code: 'UnhandledError', message: error.message },
|
|
616
|
-
statusCode: 500,
|
|
617
|
-
}),
|
|
618
|
-
)
|
|
333
|
+
if (isRedacted(error.token)) {
|
|
334
|
+
revealRedacted(error.token) // 'secret-token'
|
|
335
|
+
}
|
|
619
336
|
```
|
|
620
337
|
|
|
621
|
-
|
|
338
|
+
Use `taggedEnum` for lightweight tagged states or nested reasons that do not need to be real
|
|
339
|
+
`Error` instances. See the
|
|
340
|
+
[tagged enum guide](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#tagged-enums)
|
|
341
|
+
for the complete API.
|
|
622
342
|
|
|
623
|
-
|
|
343
|
+
## Async Work
|
|
624
344
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
| try another branch | `result.orElse(() => fallbackResult)` |
|
|
628
|
-
| replace the failure | `mapErr(...)` or `orElse(() => err(newError))` |
|
|
629
|
-
| replace the failure with success | `orElse(() => ok(defaultValue))` inside a pipeline |
|
|
630
|
-
| default at the edge | `unwrapOr(defaultValue)` |
|
|
631
|
-
| try ordered candidates | `Result.firstSuccessOf` or `ResultAsync.firstSuccessOf` |
|
|
345
|
+
`ResultAsync<T, E>` is the async counterpart to `Result<T, E>`. It is awaitable and exposes the same
|
|
346
|
+
composition style.
|
|
632
347
|
|
|
633
|
-
|
|
348
|
+
Map rejections from external code into a documented error type:
|
|
634
349
|
|
|
635
350
|
```ts
|
|
636
|
-
|
|
637
|
-
|
|
351
|
+
import { tryResultAsync } from 'resultar'
|
|
352
|
+
import type { ResultAsync } from 'resultar'
|
|
638
353
|
|
|
639
|
-
|
|
354
|
+
class DatabaseError extends createTaggedError({
|
|
355
|
+
name: 'DatabaseError',
|
|
356
|
+
message: 'Database $operation failed',
|
|
357
|
+
}) {}
|
|
640
358
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
)
|
|
359
|
+
const persistAccount = (email: string): ResultAsync<Account, DatabaseError> =>
|
|
360
|
+
tryResultAsync(
|
|
361
|
+
() => accountRepository.insert({ email }),
|
|
362
|
+
(cause) => new DatabaseError({ cause, operation: 'insert-account' }),
|
|
363
|
+
)
|
|
645
364
|
```
|
|
646
365
|
|
|
647
|
-
|
|
366
|
+
Continue from synchronous validation into async work with `asyncAndThen`:
|
|
648
367
|
|
|
649
368
|
```ts
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
369
|
+
type CreateAccountAsyncError =
|
|
370
|
+
| InvalidEmailError
|
|
371
|
+
| AccountAlreadyExistsError
|
|
372
|
+
| DatabaseError
|
|
373
|
+
|
|
374
|
+
const createAccountAsync = (
|
|
375
|
+
input: string,
|
|
376
|
+
): ResultAsync<Account, CreateAccountAsyncError> =>
|
|
377
|
+
normalizeEmail(input).andThen(ensureAccountIsAvailable).asyncAndThen(persistAccount)
|
|
656
378
|
```
|
|
657
379
|
|
|
658
|
-
|
|
380
|
+
Once a workflow is already asynchronous, `andThen` accepts callbacks that return either `Result` or
|
|
381
|
+
`ResultAsync`:
|
|
659
382
|
|
|
660
383
|
```ts
|
|
661
|
-
const
|
|
662
|
-
()
|
|
663
|
-
()
|
|
664
|
-
() =>
|
|
665
|
-
])
|
|
384
|
+
const provisioned = createAccountAsync(input)
|
|
385
|
+
.andThen(assignDefaultPlan)
|
|
386
|
+
.andThen(sendWelcomeEmail)
|
|
387
|
+
.map((account) => ({ account, status: 'ready' as const }))
|
|
666
388
|
```
|
|
667
389
|
|
|
668
|
-
|
|
390
|
+
Choose the wrapper that matches the external boundary:
|
|
669
391
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
392
|
+
| External work | Use |
|
|
393
|
+
| --- | --- |
|
|
394
|
+
| Run throwing synchronous code now | `tryResult(fn, toError)` |
|
|
395
|
+
| Wrap a throwing synchronous function | `fromThrowable(fn, toError)` |
|
|
396
|
+
| Adapt an existing promise | `fromPromise(promise, toError)` |
|
|
397
|
+
| Run a promise or async factory | `tryResultAsync(factory, toError)` |
|
|
398
|
+
| Wrap an async function | `fromThrowableAsync(fn, toError)` |
|
|
399
|
+
| Adapt a callback or subscription | `ResultAsync.fromCallback(options)` |
|
|
675
400
|
|
|
676
|
-
|
|
677
|
-
email: validateEmail(input.email),
|
|
678
|
-
name: validateName(input.name),
|
|
679
|
-
})
|
|
680
|
-
```
|
|
401
|
+
Prefer a factory with `tryResultAsync` when creating the promise can also throw synchronously.
|
|
681
402
|
|
|
682
|
-
|
|
683
|
-
sequentially by default. Pass `{ concurrency: n }` for bounded concurrency or
|
|
684
|
-
`{ concurrency: "unbounded" }` to start every mapped task immediately. See
|
|
685
|
-
[DOCUMENTATION.md](../../DOCUMENTATION.md#async-concurrency-mapping) for async concurrency details
|
|
686
|
-
and the runtime features Resultar intentionally keeps out of core.
|
|
403
|
+
## Production Async Policies
|
|
687
404
|
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
405
|
+
Resultar models common resilience policy in the expected error channel. These helpers use lazy tasks
|
|
406
|
+
so retries can start fresh work and racing helpers can pass a cooperative abort signal.
|
|
407
|
+
|
|
408
|
+
### Timeout stays typed
|
|
691
409
|
|
|
692
410
|
```ts
|
|
693
|
-
|
|
694
|
-
|
|
411
|
+
class AccountTimeoutError extends createTaggedError({
|
|
412
|
+
name: 'AccountTimeoutError',
|
|
413
|
+
message: 'Account $accountId timed out after $timeoutMs milliseconds',
|
|
414
|
+
}) {}
|
|
415
|
+
|
|
416
|
+
const account = ResultAsync.timeout(
|
|
417
|
+
(signal) => loadAccount(accountId, { signal }),
|
|
695
418
|
{
|
|
696
|
-
timeoutMs:
|
|
697
|
-
onTimeout: () => new
|
|
419
|
+
timeoutMs: 1_500,
|
|
420
|
+
onTimeout: () => new AccountTimeoutError({ accountId, timeoutMs: 1_500 }),
|
|
698
421
|
},
|
|
699
422
|
)
|
|
700
423
|
```
|
|
701
424
|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
425
|
+
If the timer wins, the timeout is returned as `Err<AccountTimeoutError>` and the task receives an
|
|
426
|
+
abort signal. No rejected timeout promise is introduced.
|
|
427
|
+
|
|
428
|
+
### Retry transient failures
|
|
705
429
|
|
|
706
430
|
```ts
|
|
707
|
-
const
|
|
708
|
-
(attempt, signal) =>
|
|
431
|
+
const account = ResultAsync.retry(
|
|
432
|
+
(attempt, signal) => loadAccount(accountId, { attempt, signal }),
|
|
709
433
|
{
|
|
710
434
|
times: 2,
|
|
711
435
|
delayMs: ({ nextAttempt }) => nextAttempt * 100,
|
|
712
436
|
jittered: 0.2,
|
|
713
|
-
while: (error) => error._tag === '
|
|
437
|
+
while: (error) => error._tag === 'DatabaseBusyError',
|
|
714
438
|
},
|
|
715
439
|
)
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
`times` is the number of retries after the first attempt. Use `retryOrElse` when retry exhaustion
|
|
443
|
+
should continue into an explicit fallback:
|
|
716
444
|
|
|
717
|
-
|
|
718
|
-
|
|
445
|
+
```ts
|
|
446
|
+
const account = ResultAsync.retryOrElse(
|
|
447
|
+
(attempt, signal) => loadAccount(accountId, { attempt, signal }),
|
|
719
448
|
{
|
|
720
449
|
times: 2,
|
|
721
|
-
orElse: () =>
|
|
450
|
+
orElse: () => loadCachedAccount(accountId),
|
|
722
451
|
},
|
|
723
452
|
)
|
|
724
453
|
```
|
|
725
454
|
|
|
726
|
-
|
|
727
|
-
`jittered: 0.2` applies a random delay from `80%` to `120%` of the computed `delayMs`.
|
|
728
|
-
|
|
729
|
-
For resourceful async work, use `ResultAsync.withResource` when acquisition and cleanup must stay
|
|
730
|
-
paired. Cleanup is best-effort and always runs after successful acquisition, including when the use
|
|
731
|
-
step returns `Err` or rejects.
|
|
455
|
+
### Race equivalent providers
|
|
732
456
|
|
|
733
457
|
```ts
|
|
734
|
-
const
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
})
|
|
739
|
-
```
|
|
740
|
-
|
|
741
|
-
Adapt callback and subscription APIs with `ResultAsync.fromCallback`. The callback source can
|
|
742
|
-
resolve or reject once, receives the cooperative signal, and returns a synchronous unsubscribe
|
|
743
|
-
function that runs after success, failure, or cancellation.
|
|
744
|
-
|
|
745
|
-
```ts
|
|
746
|
-
const nextValue = ResultAsync.fromCallback<number, SubscriptionError>({
|
|
747
|
-
catch: (cause) => new SubscriptionError({ cause }),
|
|
748
|
-
signal,
|
|
749
|
-
subscribe: ({ reject, resolve }) => {
|
|
750
|
-
const subscription = source.subscribe({ error: reject, next: resolve })
|
|
751
|
-
return () => subscription.unsubscribe()
|
|
752
|
-
},
|
|
753
|
-
})
|
|
458
|
+
const account = ResultAsync.race(
|
|
459
|
+
(signal) => loadAccountFromPrimary(accountId, { signal }),
|
|
460
|
+
(signal) => loadAccountFromReplica(accountId, { signal }),
|
|
461
|
+
)
|
|
754
462
|
```
|
|
755
463
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
[DOCUMENTATION.md](../../DOCUMENTATION.md#resourceful-async-iterables) for the full recipe.
|
|
759
|
-
|
|
760
|
-
Use `catchTag` / `catchTags` when recovery depends on a specific tagged error. Use
|
|
761
|
-
`matchTagsPartial` when only a boundary needs fallback mapping. Use `unwrapOr` only at final edges
|
|
762
|
-
where defaulting is intentional.
|
|
763
|
-
|
|
764
|
-
## Catching Errors
|
|
464
|
+
`race` returns the first success and keeps waiting after an early failure. Use `raceFirst` when the
|
|
465
|
+
first completed success or failure should win, or `raceAll` for several equivalent providers.
|
|
765
466
|
|
|
766
|
-
|
|
767
|
-
where the failure appears.
|
|
768
|
-
|
|
769
|
-
| Need | Use |
|
|
770
|
-
| --- | --- |
|
|
771
|
-
| Catch thrown sync code now | `tryResult(fn, toError?)` |
|
|
772
|
-
| Wrap a throwing sync function for later | `fromThrowable(fn, toError?)` |
|
|
773
|
-
| Catch an existing rejecting promise | `fromPromise(promise, toError)` |
|
|
774
|
-
| Adapt a callback or subscription | `ResultAsync.fromCallback({ subscribe, catch, signal? })` |
|
|
775
|
-
| Catch a promise or async factory | `tryResultAsync(promiseOrFactory, toError?)` |
|
|
776
|
-
| Wrap an async function for later | `fromThrowableAsync(fn, toError?)` |
|
|
777
|
-
| Turn one error into another | `mapErr(fn)` |
|
|
778
|
-
| Recover with another result | `orElse(fn)` |
|
|
779
|
-
| Recover specific tagged errors | `catchTag(tag, fn)` or `catchTags(handlers)` |
|
|
780
|
-
| Recover nested tagged reasons | `catchReason(parentTag, reasonTag, fn)` or `catchReasons(parentTag, handlers)` |
|
|
781
|
-
| Convert a result at a boundary | `match`, `matchTags`, or `matchTagsPartial` |
|
|
782
|
-
| Handle an existing `Error` value | `matchError` or `matchErrorPartial` |
|
|
783
|
-
| Map thrown errors in linear code | `safeTry({ try, catch })` |
|
|
784
|
-
| Recover after retry exhaustion | `ResultAsync.retryOrElse(task, options)` |
|
|
785
|
-
|
|
786
|
-
Catch uncontrolled code at the edge:
|
|
467
|
+
### Bound concurrent work
|
|
787
468
|
|
|
788
469
|
```ts
|
|
789
|
-
const
|
|
790
|
-
|
|
791
|
-
(
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
const response: StrictResultAsync<Response, FetchPayloadError> = tryResultAsync(
|
|
795
|
-
() => fetch(url),
|
|
796
|
-
(cause) => new FetchPayloadError({ cause, url }),
|
|
470
|
+
const imported = ResultAsync.forEach(
|
|
471
|
+
accountInputs,
|
|
472
|
+
(input) => createAccountAsync(input.email),
|
|
473
|
+
{ concurrency: 8, discard: true },
|
|
797
474
|
)
|
|
798
475
|
```
|
|
799
476
|
|
|
800
|
-
|
|
801
|
-
|
|
477
|
+
`forEach` is sequential by default and stops scheduling new work after the first `Err`. Pass a
|
|
478
|
+
positive concurrency number for bounded work or `"unbounded"` to start every mapped task.
|
|
802
479
|
|
|
803
|
-
|
|
480
|
+
Use mapped `validateAll` when every independent item should run and all failures should be returned:
|
|
804
481
|
|
|
805
482
|
```ts
|
|
806
|
-
const
|
|
807
|
-
|
|
808
|
-
|
|
483
|
+
const validated = ResultAsync.validateAll(
|
|
484
|
+
accountInputs,
|
|
485
|
+
(input) => normalizeEmail(input.email).asyncMap(async (email) => email),
|
|
486
|
+
{ concurrency: 8 },
|
|
487
|
+
)
|
|
809
488
|
```
|
|
810
489
|
|
|
811
|
-
|
|
812
|
-
original error as `cause`.
|
|
813
|
-
|
|
814
|
-
Handle tagged errors at the boundary:
|
|
490
|
+
### Pair acquisition with cleanup
|
|
815
491
|
|
|
816
492
|
```ts
|
|
817
|
-
const
|
|
818
|
-
(
|
|
819
|
-
{
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
(error) => ({ body: { code: error._tag }, statusCode: 500 }),
|
|
823
|
-
)
|
|
493
|
+
const account = ResultAsync.withResource({
|
|
494
|
+
acquire: (signal) => databasePool.connect({ signal }),
|
|
495
|
+
use: (connection, signal) => connection.insertAccount(input, { signal }),
|
|
496
|
+
release: (connection) => connection.close(),
|
|
497
|
+
})
|
|
824
498
|
```
|
|
825
499
|
|
|
826
|
-
|
|
827
|
-
|
|
500
|
+
After successful acquisition, `release` runs when `use` succeeds, returns `Err`, or rejects. For
|
|
501
|
+
stream-shaped work, use native `AsyncIterable<Result<T, E>>`; Resultar intentionally does not add a
|
|
502
|
+
stream runtime or scheduler.
|
|
828
503
|
|
|
829
|
-
|
|
504
|
+
See the full guides for
|
|
505
|
+
[racing and timeout](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#concurrent-racing-and-timeouts),
|
|
506
|
+
[retry](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#retrying-async-work),
|
|
507
|
+
[concurrency](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#async-concurrency-mapping),
|
|
508
|
+
and [resource cleanup](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#resourceful-async-iterables).
|
|
830
509
|
|
|
831
|
-
|
|
832
|
-
|
|
510
|
+
## Linear Workflows With `safeTry`
|
|
511
|
+
|
|
512
|
+
Use `safeTry` when a longer chain reads better as linear code. `yield*` extracts the `Ok` value and
|
|
513
|
+
short-circuits on the first `Err`.
|
|
833
514
|
|
|
834
515
|
```ts
|
|
835
|
-
import {
|
|
516
|
+
import { safeTry } from 'resultar'
|
|
517
|
+
import type { ResultAsync } from 'resultar'
|
|
836
518
|
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
519
|
+
const createAccountLinear = (
|
|
520
|
+
input: string,
|
|
521
|
+
): ResultAsync<Account, CreateAccountAsyncError> =>
|
|
522
|
+
safeTry(async function* () {
|
|
523
|
+
const email = yield* normalizeEmail(input)
|
|
524
|
+
yield* ensureAccountIsAvailable(email)
|
|
841
525
|
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
() => JSON.parse(input) as Config,
|
|
845
|
-
(cause) => new ParseConfigError({ cause }),
|
|
846
|
-
)
|
|
526
|
+
return persistAccount(email)
|
|
527
|
+
})
|
|
847
528
|
```
|
|
848
529
|
|
|
849
|
-
|
|
530
|
+
The object form can map unexpected throws from inside the generator while leaving yielded `Err`
|
|
531
|
+
values unchanged:
|
|
850
532
|
|
|
851
533
|
```ts
|
|
852
|
-
const
|
|
534
|
+
const account = safeTry({
|
|
535
|
+
async *try() {
|
|
536
|
+
const email = yield* normalizeEmail(input)
|
|
537
|
+
return persistAccount(email)
|
|
538
|
+
},
|
|
539
|
+
catch: (cause) => new AccountServiceError({ cause, email: input }),
|
|
540
|
+
})
|
|
853
541
|
```
|
|
854
542
|
|
|
855
|
-
|
|
856
|
-
form is useful for infrastructure helpers because the task and error mapper stay together:
|
|
543
|
+
Prefer `yield* result`. Compatibility helpers such as `safeUnwrap` are not needed in new workflows.
|
|
857
544
|
|
|
858
|
-
|
|
859
|
-
import { createTaggedError, tryResultAsync } from 'resultar'
|
|
860
|
-
import type { StrictResultAsync } from 'resultar'
|
|
545
|
+
## Recover Inside A Workflow
|
|
861
546
|
|
|
862
|
-
|
|
547
|
+
Use recovery methods when a failure has a useful local alternative.
|
|
863
548
|
|
|
864
|
-
|
|
865
|
-
name: 'TaskError',
|
|
866
|
-
message: 'Task failed: $label',
|
|
867
|
-
}) {}
|
|
549
|
+
Recover from one tagged error with `catchTag`:
|
|
868
550
|
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
551
|
+
```ts
|
|
552
|
+
const account = createAccountAsync(input).catchTag(
|
|
553
|
+
'AccountAlreadyExistsError',
|
|
554
|
+
(error) => findAccountByEmail(String(error.email)),
|
|
555
|
+
)
|
|
874
556
|
```
|
|
875
557
|
|
|
876
|
-
|
|
877
|
-
throwing expected failures.
|
|
878
|
-
|
|
879
|
-
## Validation Errors
|
|
880
|
-
|
|
881
|
-
Keep validation failures as normal tagged errors. Map external validators into a small issue shape
|
|
882
|
-
at the adapter boundary.
|
|
558
|
+
Recover several tagged errors with `catchTags`:
|
|
883
559
|
|
|
884
560
|
```ts
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
class ValidationError extends createTaggedError({
|
|
891
|
-
name: 'ValidationError',
|
|
892
|
-
message: 'Invalid input',
|
|
893
|
-
}) {
|
|
894
|
-
public readonly issues: readonly ValidationIssue[]
|
|
895
|
-
|
|
896
|
-
public constructor(props: {
|
|
897
|
-
readonly cause?: unknown
|
|
898
|
-
readonly issues: readonly ValidationIssue[]
|
|
899
|
-
}) {
|
|
900
|
-
super({ cause: props.cause })
|
|
901
|
-
this.issues = props.issues
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
const issueFromPath = (path: readonly (number | string)[], message: string): ValidationIssue => ({
|
|
906
|
-
message,
|
|
907
|
-
path: path.join('.'),
|
|
561
|
+
const account = createAccountAsync(input).catchTags({
|
|
562
|
+
AccountAlreadyExistsError: (error) => findAccountByEmail(String(error.email)),
|
|
563
|
+
InvalidEmailError: (error) =>
|
|
564
|
+
ok({ email: String(error.email), id: 'draft_account' }),
|
|
908
565
|
})
|
|
909
566
|
```
|
|
910
567
|
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
## Unexpected Errors
|
|
568
|
+
Unhandled tags remain in the error channel. Use `mapErr` when the failure should be translated and
|
|
569
|
+
`orElse` when recovery should run another Result-producing branch.
|
|
915
570
|
|
|
916
|
-
|
|
917
|
-
|
|
571
|
+
```ts
|
|
572
|
+
const account = loadCachedAccount(accountId)
|
|
573
|
+
.orElse(() => loadAccount(accountId))
|
|
574
|
+
.mapErr((cause) => new AccountServiceError({ cause, email: input }))
|
|
575
|
+
```
|
|
918
576
|
|
|
919
|
-
Use
|
|
577
|
+
Use recovery inside a workflow. At an application boundary, consume the final result with `match`,
|
|
578
|
+
`matchTags`, or `matchTagsPartial`.
|
|
920
579
|
|
|
921
|
-
|
|
922
|
-
`fromThrowable`, `fromThrowableAsync`, or `fromPromise`.
|
|
923
|
-
- Keep your own recoverable domain failures as `Err`.
|
|
924
|
-
- Use `unwrapOrThrow()` only at final edges where turning an `Err` into a thrown error is deliberate.
|
|
580
|
+
## Handle Results At Boundaries
|
|
925
581
|
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
```
|
|
582
|
+
Boundary code turns a Result into an HTTP response, queue acknowledgement, CLI exit code, log entry,
|
|
583
|
+
or UI state.
|
|
929
584
|
|
|
930
|
-
|
|
585
|
+
Use `matchTags` when tagged errors should be handled exhaustively:
|
|
931
586
|
|
|
932
587
|
```ts
|
|
933
|
-
const
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
588
|
+
const response = await createAccountAsync(input).matchTags(
|
|
589
|
+
(account) => ({
|
|
590
|
+
body: account,
|
|
591
|
+
statusCode: 201,
|
|
592
|
+
}),
|
|
593
|
+
{
|
|
594
|
+
AccountAlreadyExistsError: (error) => ({
|
|
595
|
+
body: { code: error._tag, message: error.message },
|
|
596
|
+
statusCode: 409,
|
|
597
|
+
}),
|
|
598
|
+
DatabaseError: (error) => ({
|
|
599
|
+
body: { code: error._tag, message: error.message },
|
|
600
|
+
statusCode: 503,
|
|
601
|
+
}),
|
|
602
|
+
InvalidEmailError: (error) => ({
|
|
603
|
+
body: { code: error._tag, message: error.message },
|
|
604
|
+
statusCode: 400,
|
|
605
|
+
}),
|
|
937
606
|
},
|
|
938
|
-
|
|
939
|
-
})
|
|
607
|
+
)
|
|
940
608
|
```
|
|
941
609
|
|
|
942
|
-
|
|
610
|
+
If a handler is missing, TypeScript reports it. If only selected errors need special handling, use
|
|
611
|
+
`matchTagsPartial` with a fallback. Use plain `match` when the distinction between individual error
|
|
612
|
+
tags does not matter.
|
|
943
613
|
|
|
944
|
-
|
|
945
|
-
|
|
614
|
+
Avoid unwrapping in normal application flows. `unwrapOr`, `unwrapOrThrow`, `_unsafeUnwrap`, and
|
|
615
|
+
`_unsafeUnwrapErr` are best reserved for deliberate final boundaries or tests.
|
|
946
616
|
|
|
947
|
-
|
|
948
|
-
import { safeTry } from 'resultar'
|
|
617
|
+
## Strict Results At Production Boundaries
|
|
949
618
|
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
): StrictResult<User, InvalidEmailError | UserAlreadyExistsError> =>
|
|
953
|
-
safeTry(function* () {
|
|
954
|
-
const validEmail = yield* validateEmail(email)
|
|
955
|
-
const availableEmail = yield* ensureUserDoesNotExist(validEmail)
|
|
619
|
+
`Result<T, E>` intentionally allows any failure type. Strings, enums, and small objects can be useful
|
|
620
|
+
inside narrow local workflows.
|
|
956
621
|
|
|
957
|
-
|
|
958
|
-
})
|
|
959
|
-
```
|
|
960
|
-
|
|
961
|
-
The same `safeTry` helper supports async generators:
|
|
622
|
+
For service, HTTP, job, queue, CLI, and integration boundaries, prefer the Error-only aliases:
|
|
962
623
|
|
|
963
624
|
```ts
|
|
964
|
-
|
|
965
|
-
email: string,
|
|
966
|
-
): StrictResultAsync<User, InvalidEmailError | UserAlreadyExistsError | DatabaseError> =>
|
|
967
|
-
safeTry(async function* () {
|
|
968
|
-
const validEmail = yield* validateEmail(email)
|
|
969
|
-
const availableEmail = yield* ensureUserDoesNotExist(validEmail)
|
|
970
|
-
|
|
971
|
-
return insertUserAsync(availableEmail)
|
|
972
|
-
})
|
|
973
|
-
```
|
|
974
|
-
|
|
975
|
-
The object form can map unexpected thrown errors while leaving yielded `Err` values unchanged:
|
|
625
|
+
import type { StrictResult, StrictResultAsync } from 'resultar'
|
|
976
626
|
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
const raw = yield* readConfigFile()
|
|
627
|
+
const validateAccount = (
|
|
628
|
+
input: string,
|
|
629
|
+
): StrictResult<string, InvalidEmailError> => normalizeEmail(input)
|
|
981
630
|
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
})
|
|
631
|
+
const provisionAccount = (
|
|
632
|
+
input: string,
|
|
633
|
+
): StrictResultAsync<Account, CreateAccountAsyncError> => createAccountAsync(input)
|
|
986
634
|
```
|
|
987
635
|
|
|
988
|
-
|
|
636
|
+
`StrictResult<T, E extends Error>` and `StrictResultAsync<T, E extends Error>` are type-only aliases.
|
|
637
|
+
They keep the same Resultar API while documenting that failures carry standard `Error` behavior:
|
|
638
|
+
messages, causes, stack traces, and structured metadata.
|
|
989
639
|
|
|
990
|
-
|
|
991
|
-
|
|
640
|
+
## Prevent Ignored Results
|
|
641
|
+
|
|
642
|
+
Result values are useful only when callers handle them. Install `resultar-check` with a project-local
|
|
643
|
+
TypeScript 7+:
|
|
992
644
|
|
|
993
645
|
```sh
|
|
994
646
|
pnpm add -D resultar-check "typescript@>=7"
|
|
995
647
|
```
|
|
996
648
|
|
|
997
|
-
|
|
649
|
+
Use the CLI as the authoritative local and CI check:
|
|
998
650
|
|
|
999
651
|
```json
|
|
1000
652
|
{
|
|
@@ -1004,151 +656,89 @@ Add a check script:
|
|
|
1004
656
|
}
|
|
1005
657
|
```
|
|
1006
658
|
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
These fail in the default `must-use` mode:
|
|
1010
|
-
|
|
1011
|
-
```ts
|
|
1012
|
-
saveUser(input)
|
|
1013
|
-
const result = saveUser(input)
|
|
1014
|
-
```
|
|
1015
|
-
|
|
1016
|
-
These are intentional:
|
|
1017
|
-
|
|
1018
|
-
```ts
|
|
1019
|
-
return saveUser(input)
|
|
1020
|
-
void saveUser(input)
|
|
1021
|
-
saveUser(input).match(handleSaved, handleError)
|
|
1022
|
-
```
|
|
1023
|
-
|
|
1024
|
-
The default mode is neverthrow-style `must-use`: it also reports assigned `Result` values that are
|
|
1025
|
-
only passed around and never consumed with `match`, `unwrapOr`, `_unsafeUnwrap`, `isOk`, `isErr`,
|
|
1026
|
-
returned, or explicitly discarded. Use `--mode direct` for the lower-noise expression-only check.
|
|
1027
|
-
|
|
1028
|
-
Configure Resultar rules in `tsconfig.json`:
|
|
659
|
+
The CLI runs TypeScript first and then Resultar diagnostics over the same `tsconfig.json`. Configure
|
|
660
|
+
the TypeScript language-server plugin for equivalent feedback while editing:
|
|
1029
661
|
|
|
1030
662
|
```json
|
|
1031
663
|
{
|
|
1032
664
|
"$schema": "./node_modules/resultar-check/schema.json",
|
|
1033
665
|
"compilerOptions": {
|
|
1034
|
-
"plugins": [
|
|
666
|
+
"plugins": [
|
|
667
|
+
{
|
|
668
|
+
"name": "resultar-check",
|
|
669
|
+
"noDiscard": "error"
|
|
670
|
+
}
|
|
671
|
+
]
|
|
1035
672
|
}
|
|
1036
673
|
}
|
|
1037
674
|
```
|
|
1038
675
|
|
|
1039
|
-
|
|
1040
|
-
|
|
676
|
+
After the CLI and language server are working, add the Oxlint, ESLint, or Deno Lint adapter only if
|
|
677
|
+
you want optional low-latency syntax feedback. The adapters intentionally expose fewer rules because
|
|
678
|
+
they do not have TypeScript type information.
|
|
679
|
+
|
|
680
|
+
See the
|
|
681
|
+
[`resultar-check` guide](https://github.com/inaiat/resultar/blob/main/packages/check/README.md)
|
|
682
|
+
for all rules, editor setup, ignore patterns, and lint adapter configuration.
|
|
683
|
+
|
|
684
|
+
## HTTP Request Packages
|
|
1041
685
|
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
The `examples/lint` package shows how to compare Oxlint, ESLint, and CLI counts by running the CLI
|
|
1045
|
-
with the same AST-only rule subset.
|
|
686
|
+
Resultar's Fetch-first request packages return `ResultAsync` values for request creation, network,
|
|
687
|
+
HTTP status, JSON parsing, validation, and retry failures.
|
|
1046
688
|
|
|
1047
|
-
|
|
689
|
+
| Package | Use it when | Guide |
|
|
690
|
+
| --- | --- | --- |
|
|
691
|
+
| [`resultar-request`](https://www.npmjs.com/package/resultar-request) | You use a custom validator or decoder | [README](https://github.com/inaiat/resultar/blob/main/packages/request/README.md) |
|
|
692
|
+
| [`resultar-request-typebox`](https://www.npmjs.com/package/resultar-request-typebox) | Your response contract is a TypeBox schema | [README](https://github.com/inaiat/resultar/blob/main/packages/request-typebox/README.md) |
|
|
693
|
+
| [`resultar-request-zod`](https://www.npmjs.com/package/resultar-request-zod) | Your response contract is a Zod schema or transform | [README](https://github.com/inaiat/resultar/blob/main/packages/request-zod/README.md) |
|
|
694
|
+
|
|
695
|
+
The adapters delegate transport, JSON parsing, retries, and error mapping to `resultar-request` and
|
|
696
|
+
re-export its public request types. Install only the core request package and schema adapter your
|
|
697
|
+
service needs.
|
|
1048
698
|
|
|
1049
699
|
## API Decision Guide
|
|
1050
700
|
|
|
1051
701
|
| Need | Use |
|
|
1052
702
|
| --- | --- |
|
|
1053
|
-
| Create
|
|
1054
|
-
| Create
|
|
1055
|
-
|
|
|
1056
|
-
|
|
|
1057
|
-
|
|
|
1058
|
-
|
|
|
1059
|
-
|
|
|
1060
|
-
|
|
|
1061
|
-
|
|
|
1062
|
-
|
|
|
1063
|
-
|
|
|
1064
|
-
|
|
|
1065
|
-
|
|
|
1066
|
-
|
|
|
1067
|
-
|
|
|
1068
|
-
|
|
|
1069
|
-
|
|
|
1070
|
-
|
|
|
1071
|
-
|
|
|
1072
|
-
|
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
- No generator-based runtime DSL.
|
|
1093
|
-
- No schedule engine in core.
|
|
1094
|
-
- No config, cache, request resolver, resource, fiber, or service runtime.
|
|
1095
|
-
- No raw `Error | T` conversion API.
|
|
1096
|
-
|
|
1097
|
-
## Version 3 Notes
|
|
1098
|
-
|
|
1099
|
-
`3.0.0` is a semver-major release:
|
|
1100
|
-
|
|
1101
|
-
- `no-discard` tooling moved out of the runtime package and into dedicated packages.
|
|
1102
|
-
- TypeScript peer support is now `>=6.0.0`.
|
|
1103
|
-
- Deprecated APIs are not part of the current API:
|
|
1104
|
-
|
|
1105
|
-
- `Result#finally`
|
|
1106
|
-
- `ResultAsync#finally`
|
|
1107
|
-
- `ResultAsync#safeUnwrap`
|
|
1108
|
-
- `safeTryAsync`
|
|
1109
|
-
|
|
1110
|
-
Use `log`, `toDisposable`, `toAsyncDisposable`, and `safeTry(async function* () { ... })` instead.
|
|
1111
|
-
`Result#safeUnwrap()` remains available for compatibility, but new code should prefer
|
|
1112
|
-
`yield* result`.
|
|
1113
|
-
|
|
1114
|
-
`tryCatch` and `tryCatchAsync` remain exported as backward-compatible aliases. Prefer `tryResult`
|
|
1115
|
-
and `tryResultAsync` in new code and docs.
|
|
1116
|
-
|
|
1117
|
-
## Documentation
|
|
1118
|
-
|
|
1119
|
-
This README is the runtime package entry point. For a full guide with larger examples and the
|
|
1120
|
-
complete API map, see the repository documentation.
|
|
1121
|
-
|
|
1122
|
-
More focused material:
|
|
1123
|
-
|
|
1124
|
-
- [Full guide](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md)
|
|
1125
|
-
- [Type-safe error handling article, English](https://github.com/inaiat/resultar/blob/main/articles/en/type-safe.md)
|
|
1126
|
-
- [Artigo sobre tratamento de erros type-safe, Portuguese](https://github.com/inaiat/resultar/blob/main/articles/pt/type-safe.md)
|
|
1127
|
-
- [resultar-check package guide](https://github.com/inaiat/resultar/blob/main/packages/check/README.md)
|
|
1128
|
-
|
|
1129
|
-
## Repository
|
|
1130
|
-
|
|
1131
|
-
This repository is a pnpm workspace:
|
|
1132
|
-
|
|
1133
|
-
- `packages/resultar`: Resultar runtime package.
|
|
1134
|
-
- `packages/check`: TypeScript >=7 plus Resultar validation and AST-only lint adapters.
|
|
1135
|
-
- `benchmarks`: benchmark package.
|
|
1136
|
-
- `examples/resultar`: runnable core Resultar cookbook.
|
|
1137
|
-
- `examples/lint`: adapter parity smoke for AST-only Resultar rules across Oxlint, ESLint, and `resultar-check` CLI.
|
|
1138
|
-
|
|
1139
|
-
Common commands:
|
|
1140
|
-
|
|
1141
|
-
```sh
|
|
1142
|
-
pnpm install
|
|
1143
|
-
pnpm run fmt:check
|
|
1144
|
-
pnpm run check
|
|
1145
|
-
pnpm test
|
|
1146
|
-
pnpm run test:cov
|
|
1147
|
-
pnpm run build
|
|
1148
|
-
pnpm run check:full
|
|
1149
|
-
pnpm run test:examples
|
|
1150
|
-
pnpm run bench
|
|
1151
|
-
```
|
|
703
|
+
| Create success or failure | `ok`, `err`, `okAsync`, `errAsync` |
|
|
704
|
+
| Create an `Ok(undefined)` | `unit`, `unitAsync` |
|
|
705
|
+
| Transform success | `map`, `asyncMap`, `as` |
|
|
706
|
+
| Add a fallible condition | `filterOrElse` |
|
|
707
|
+
| Transform failure | `mapErr` |
|
|
708
|
+
| Continue fallible work | `andThen`, `asyncAndThen` |
|
|
709
|
+
| Recover from failure | `orElse`, `catchTag`, `catchTags` |
|
|
710
|
+
| Wrap throwing or rejecting code | `tryResult`, `tryResultAsync`, `fromPromise` |
|
|
711
|
+
| Write linear Result code | `safeTry` |
|
|
712
|
+
| Handle a final boundary | `match`, `matchTags`, `matchTagsPartial` |
|
|
713
|
+
| Combine independent results | `zip`, `combine`, `combineWithAllErrors` |
|
|
714
|
+
| Try ordered fallback candidates | `firstSuccessOf` |
|
|
715
|
+
| Process async collections | `ResultAsync.forEach`, `ResultAsync.validateAll` |
|
|
716
|
+
| Race concurrent tasks | `ResultAsync.race`, `raceAll`, `raceFirst` |
|
|
717
|
+
| Apply a timeout | `ResultAsync.timeout` |
|
|
718
|
+
| Retry transient work | `ResultAsync.retry`, `retryOrElse` |
|
|
719
|
+
| Pair acquisition and cleanup | `ResultAsync.withResource` |
|
|
720
|
+
| Observe without changing the result | `tap`, `tapError`, `log` |
|
|
721
|
+
| Default deliberately at an edge | `unwrapOr` |
|
|
722
|
+
| Throw deliberately at an edge | `unwrapOrThrow` |
|
|
723
|
+
|
|
724
|
+
The
|
|
725
|
+
[complete API map](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#api-map)
|
|
726
|
+
covers overloads, aliases, callback semantics, collection helpers, conditional helpers, tagged
|
|
727
|
+
reasons, disposable results, and compatibility APIs.
|
|
728
|
+
|
|
729
|
+
## More Documentation
|
|
730
|
+
|
|
731
|
+
- [Full Resultar guide](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md)
|
|
732
|
+
- [Runnable core cookbook](https://github.com/inaiat/resultar/tree/main/examples/resultar)
|
|
733
|
+
- [Catching and recovering errors](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#catching-and-recovering-errors)
|
|
734
|
+
- [Safe Try](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#safe-try)
|
|
735
|
+
- [Validation error recipes](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#validation-error-recipes)
|
|
736
|
+
- [Coming from other error-handling styles](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#coming-from-other-styles)
|
|
737
|
+
- [Public exports and compatibility aliases](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#public-entry-point)
|
|
738
|
+
- [Type-safe error handling article](https://github.com/inaiat/resultar/blob/main/articles/en/type-safe.md)
|
|
739
|
+
- [Artigo sobre tratamento de erros type-safe](https://github.com/inaiat/resultar/blob/main/articles/pt/type-safe.md)
|
|
740
|
+
|
|
741
|
+
Resultar is also published on [JSR as `@inaiat/resultar`](https://jsr.io/@inaiat/resultar).
|
|
1152
742
|
|
|
1153
743
|
## License
|
|
1154
744
|
|