resultar 3.4.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 +453 -846
- package/dist/index.d.ts +51 -3
- package/dist/index.js +354 -262
- package/package.json +10 -10
- package/dist/index.js.map +0 -1
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,927 +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(
|
|
103
|
-
|
|
104
|
-
return Number.isInteger(port) && port > 0 ? ok(port) : InvalidPortError.err({ value })
|
|
105
|
-
}
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
Callers now know the success type and the expected error type without reading the implementation.
|
|
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:
|
|
130
|
-
|
|
131
|
-
```ts
|
|
132
|
-
const getPosts = async (url: string) => {
|
|
133
|
-
try {
|
|
134
|
-
const response = await fetch(url)
|
|
135
|
-
|
|
136
|
-
if (response.status >= 400) {
|
|
137
|
-
throw new HttpClientError(response.status)
|
|
138
|
-
}
|
|
104
|
+
const parsePort = (input: string): Result<number, InvalidPortError> => {
|
|
105
|
+
const port = Number(input)
|
|
139
106
|
|
|
140
|
-
|
|
141
|
-
} catch (cause) {
|
|
142
|
-
throw new InfrastructureError({ cause })
|
|
143
|
-
}
|
|
107
|
+
return Number.isInteger(port) && port > 0 ? ok(port) : InvalidPortError.err({ input })
|
|
144
108
|
}
|
|
145
109
|
```
|
|
146
110
|
|
|
147
|
-
|
|
111
|
+
The caller can now see both outcomes without reading the implementation:
|
|
148
112
|
|
|
149
113
|
```ts
|
|
150
|
-
const
|
|
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
|
-
)
|
|
114
|
+
const port = parsePort(process.env.PORT ?? '3000')
|
|
182
115
|
|
|
183
|
-
|
|
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
|
-
|
|
222
|
-
|
|
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
|
-
```
|
|
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.
|
|
232
125
|
|
|
233
|
-
|
|
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
|
-
}) {}
|
|
259
|
+
import type { Result } from 'resultar'
|
|
498
260
|
|
|
499
|
-
const
|
|
500
|
-
|
|
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
|
-
```
|
|
507
|
-
|
|
508
|
-
Use `pipe` when a transform should be named and reused. A pipe step receives the current
|
|
509
|
-
`Result`/`ResultAsync` and returns the next value in the chain:
|
|
510
|
-
|
|
511
|
-
```ts
|
|
512
|
-
import type { Result, StrictResult } from 'resultar'
|
|
261
|
+
const auditAccount = <E>(result: Result<Account, E>): Result<Account, E> =>
|
|
262
|
+
result.tap((account) => logger.info({ accountId: account.id }, 'account ready'))
|
|
513
263
|
|
|
514
|
-
const
|
|
515
|
-
|
|
264
|
+
const accountEmail = createAccount(input)
|
|
265
|
+
.pipe(auditAccount)
|
|
266
|
+
.map((account) => account.email)
|
|
267
|
+
```
|
|
516
268
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
result.filterOrElse(
|
|
521
|
-
(email) => email.endsWith('@company.com'),
|
|
522
|
-
(email) => new InvalidDomainError({ domain: email.split('@')[1] ?? 'unknown' }),
|
|
523
|
-
)
|
|
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.
|
|
524
272
|
|
|
525
|
-
|
|
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
|
-
|
|
533
|
-
|
|
534
|
-
const auditUser =
|
|
535
|
-
<E>(result: ResultAsync<User, E>): ResultAsync<User, E> =>
|
|
536
|
-
result.tap((user) => logger.info({ userId: user.id }, 'user created'))
|
|
278
|
+
const error = new AccountAlreadyExistsError({ email: 'person@example.com' })
|
|
537
279
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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
|
-
|
|
545
|
-
|
|
546
|
-
```ts
|
|
547
|
-
const draftUser = createUser('bad-email').catchTag('InvalidEmailError', (error) =>
|
|
548
|
-
ok({
|
|
549
|
-
email: error.email,
|
|
550
|
-
id: 'draft_user',
|
|
551
|
-
}),
|
|
552
|
-
)
|
|
553
|
-
```
|
|
289
|
+
Tagged errors support:
|
|
554
290
|
|
|
555
|
-
|
|
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.
|
|
556
299
|
|
|
557
|
-
|
|
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
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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
|
+
}) {}
|
|
307
|
+
|
|
308
|
+
const databaseError = new DatabaseError({
|
|
309
|
+
cause: new Error('connection refused'),
|
|
310
|
+
operation: 'insert-account',
|
|
311
|
+
})
|
|
586
312
|
|
|
587
|
-
|
|
588
|
-
err(new AiError({ reason: AiReason.RateLimitError({ retryAfterMs: 1_000 }) }))
|
|
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
|
-
|
|
678
|
-
|
|
679
|
-
})
|
|
680
|
-
```
|
|
401
|
+
Prefer a factory with `tryResultAsync` when creating the promise can also throw synchronously.
|
|
402
|
+
|
|
403
|
+
## Production Async Policies
|
|
681
404
|
|
|
682
|
-
|
|
683
|
-
|
|
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.
|
|
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.
|
|
687
407
|
|
|
688
|
-
|
|
689
|
-
success. `raceFirst` returns the first completed result, success or failure. `timeout` is a
|
|
690
|
-
`raceFirst` convenience.
|
|
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
|
+
```
|
|
716
441
|
|
|
717
|
-
|
|
718
|
-
|
|
442
|
+
`times` is the number of retries after the first attempt. Use `retryOrElse` when retry exhaustion
|
|
443
|
+
should continue into an explicit fallback:
|
|
444
|
+
|
|
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
|
-
})
|
|
458
|
+
const account = ResultAsync.race(
|
|
459
|
+
(signal) => loadAccountFromPrimary(accountId, { signal }),
|
|
460
|
+
(signal) => loadAccountFromReplica(accountId, { signal }),
|
|
461
|
+
)
|
|
739
462
|
```
|
|
740
463
|
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
[DOCUMENTATION.md](../../DOCUMENTATION.md#resourceful-async-iterables) for the full recipe.
|
|
744
|
-
|
|
745
|
-
Use `catchTag` / `catchTags` when recovery depends on a specific tagged error. Use
|
|
746
|
-
`matchTagsPartial` when only a boundary needs fallback mapping. Use `unwrapOr` only at final edges
|
|
747
|
-
where defaulting is intentional.
|
|
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.
|
|
748
466
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
Resultar has different catch APIs for different error sources. Use the smallest API that matches
|
|
752
|
-
where the failure appears.
|
|
753
|
-
|
|
754
|
-
| Need | Use |
|
|
755
|
-
| --- | --- |
|
|
756
|
-
| Catch thrown sync code now | `tryResult(fn, toError?)` |
|
|
757
|
-
| Wrap a throwing sync function for later | `fromThrowable(fn, toError?)` |
|
|
758
|
-
| Catch an existing rejecting promise | `fromPromise(promise, toError)` |
|
|
759
|
-
| Catch a promise or async factory | `tryResultAsync(promiseOrFactory, toError?)` |
|
|
760
|
-
| Wrap an async function for later | `fromThrowableAsync(fn, toError?)` |
|
|
761
|
-
| Turn one error into another | `mapErr(fn)` |
|
|
762
|
-
| Recover with another result | `orElse(fn)` |
|
|
763
|
-
| Recover specific tagged errors | `catchTag(tag, fn)` or `catchTags(handlers)` |
|
|
764
|
-
| Recover nested tagged reasons | `catchReason(parentTag, reasonTag, fn)` or `catchReasons(parentTag, handlers)` |
|
|
765
|
-
| Convert a result at a boundary | `match`, `matchTags`, or `matchTagsPartial` |
|
|
766
|
-
| Handle an existing `Error` value | `matchError` or `matchErrorPartial` |
|
|
767
|
-
| Map thrown errors in linear code | `safeTry({ try, catch })` |
|
|
768
|
-
| Recover after retry exhaustion | `ResultAsync.retryOrElse(task, options)` |
|
|
769
|
-
|
|
770
|
-
Catch uncontrolled code at the edge:
|
|
467
|
+
### Bound concurrent work
|
|
771
468
|
|
|
772
469
|
```ts
|
|
773
|
-
const
|
|
774
|
-
|
|
775
|
-
(
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
const response: StrictResultAsync<Response, FetchPayloadError> = tryResultAsync(
|
|
779
|
-
() => fetch(url),
|
|
780
|
-
(cause) => new FetchPayloadError({ cause, url }),
|
|
470
|
+
const imported = ResultAsync.forEach(
|
|
471
|
+
accountInputs,
|
|
472
|
+
(input) => createAccountAsync(input.email),
|
|
473
|
+
{ concurrency: 8, discard: true },
|
|
781
474
|
)
|
|
782
475
|
```
|
|
783
476
|
|
|
784
|
-
|
|
785
|
-
|
|
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.
|
|
786
479
|
|
|
787
|
-
|
|
480
|
+
Use mapped `validateAll` when every independent item should run and all failures should be returned:
|
|
788
481
|
|
|
789
482
|
```ts
|
|
790
|
-
const
|
|
791
|
-
|
|
792
|
-
|
|
483
|
+
const validated = ResultAsync.validateAll(
|
|
484
|
+
accountInputs,
|
|
485
|
+
(input) => normalizeEmail(input.email).asyncMap(async (email) => email),
|
|
486
|
+
{ concurrency: 8 },
|
|
487
|
+
)
|
|
793
488
|
```
|
|
794
489
|
|
|
795
|
-
|
|
796
|
-
original error as `cause`.
|
|
797
|
-
|
|
798
|
-
Handle tagged errors at the boundary:
|
|
490
|
+
### Pair acquisition with cleanup
|
|
799
491
|
|
|
800
492
|
```ts
|
|
801
|
-
const
|
|
802
|
-
(
|
|
803
|
-
{
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
(error) => ({ body: { code: error._tag }, statusCode: 500 }),
|
|
807
|
-
)
|
|
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
|
+
})
|
|
808
498
|
```
|
|
809
499
|
|
|
810
|
-
|
|
811
|
-
|
|
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.
|
|
812
503
|
|
|
813
|
-
|
|
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).
|
|
814
509
|
|
|
815
|
-
|
|
816
|
-
|
|
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`.
|
|
817
514
|
|
|
818
515
|
```ts
|
|
819
|
-
import {
|
|
516
|
+
import { safeTry } from 'resultar'
|
|
517
|
+
import type { ResultAsync } from 'resultar'
|
|
820
518
|
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
519
|
+
const createAccountLinear = (
|
|
520
|
+
input: string,
|
|
521
|
+
): ResultAsync<Account, CreateAccountAsyncError> =>
|
|
522
|
+
safeTry(async function* () {
|
|
523
|
+
const email = yield* normalizeEmail(input)
|
|
524
|
+
yield* ensureAccountIsAvailable(email)
|
|
825
525
|
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
() => JSON.parse(input) as Config,
|
|
829
|
-
(cause) => new ParseConfigError({ cause }),
|
|
830
|
-
)
|
|
526
|
+
return persistAccount(email)
|
|
527
|
+
})
|
|
831
528
|
```
|
|
832
529
|
|
|
833
|
-
|
|
530
|
+
The object form can map unexpected throws from inside the generator while leaving yielded `Err`
|
|
531
|
+
values unchanged:
|
|
834
532
|
|
|
835
533
|
```ts
|
|
836
|
-
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
|
+
})
|
|
837
541
|
```
|
|
838
542
|
|
|
839
|
-
|
|
840
|
-
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.
|
|
841
544
|
|
|
842
|
-
|
|
843
|
-
import { createTaggedError, tryResultAsync } from 'resultar'
|
|
844
|
-
import type { StrictResultAsync } from 'resultar'
|
|
545
|
+
## Recover Inside A Workflow
|
|
845
546
|
|
|
846
|
-
|
|
547
|
+
Use recovery methods when a failure has a useful local alternative.
|
|
847
548
|
|
|
848
|
-
|
|
849
|
-
name: 'TaskError',
|
|
850
|
-
message: 'Task failed: $label',
|
|
851
|
-
}) {}
|
|
549
|
+
Recover from one tagged error with `catchTag`:
|
|
852
550
|
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
551
|
+
```ts
|
|
552
|
+
const account = createAccountAsync(input).catchTag(
|
|
553
|
+
'AccountAlreadyExistsError',
|
|
554
|
+
(error) => findAccountByEmail(String(error.email)),
|
|
555
|
+
)
|
|
858
556
|
```
|
|
859
557
|
|
|
860
|
-
|
|
861
|
-
throwing expected failures.
|
|
862
|
-
|
|
863
|
-
## Validation Errors
|
|
864
|
-
|
|
865
|
-
Keep validation failures as normal tagged errors. Map external validators into a small issue shape
|
|
866
|
-
at the adapter boundary.
|
|
558
|
+
Recover several tagged errors with `catchTags`:
|
|
867
559
|
|
|
868
560
|
```ts
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
class ValidationError extends createTaggedError({
|
|
875
|
-
name: 'ValidationError',
|
|
876
|
-
message: 'Invalid input',
|
|
877
|
-
}) {
|
|
878
|
-
public readonly issues: readonly ValidationIssue[]
|
|
879
|
-
|
|
880
|
-
public constructor(props: {
|
|
881
|
-
readonly cause?: unknown
|
|
882
|
-
readonly issues: readonly ValidationIssue[]
|
|
883
|
-
}) {
|
|
884
|
-
super({ cause: props.cause })
|
|
885
|
-
this.issues = props.issues
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
const issueFromPath = (path: readonly (number | string)[], message: string): ValidationIssue => ({
|
|
890
|
-
message,
|
|
891
|
-
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' }),
|
|
892
565
|
})
|
|
893
566
|
```
|
|
894
567
|
|
|
895
|
-
|
|
896
|
-
|
|
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.
|
|
897
570
|
|
|
898
|
-
|
|
571
|
+
```ts
|
|
572
|
+
const account = loadCachedAccount(accountId)
|
|
573
|
+
.orElse(() => loadAccount(accountId))
|
|
574
|
+
.mapErr((cause) => new AccountServiceError({ cause, email: input }))
|
|
575
|
+
```
|
|
899
576
|
|
|
900
|
-
|
|
901
|
-
|
|
577
|
+
Use recovery inside a workflow. At an application boundary, consume the final result with `match`,
|
|
578
|
+
`matchTags`, or `matchTagsPartial`.
|
|
902
579
|
|
|
903
|
-
|
|
580
|
+
## Handle Results At Boundaries
|
|
904
581
|
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
- Keep your own recoverable domain failures as `Err`.
|
|
908
|
-
- Use `unwrapOrThrow()` only at final edges where turning an `Err` into a thrown error is deliberate.
|
|
582
|
+
Boundary code turns a Result into an HTTP response, queue acknowledgement, CLI exit code, log entry,
|
|
583
|
+
or UI state.
|
|
909
584
|
|
|
910
|
-
|
|
911
|
-
const user = await fetchUser('usr_123').unwrapOrThrow()
|
|
912
|
-
```
|
|
913
|
-
|
|
914
|
-
Inside `safeTry`, the object form can map thrown bugs from the generator body into an explicit error:
|
|
585
|
+
Use `matchTags` when tagged errors should be handled exhaustively:
|
|
915
586
|
|
|
916
587
|
```ts
|
|
917
|
-
const
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
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
|
+
}),
|
|
921
606
|
},
|
|
922
|
-
|
|
923
|
-
})
|
|
607
|
+
)
|
|
924
608
|
```
|
|
925
609
|
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
short-circuits on the first `Err`.
|
|
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.
|
|
930
613
|
|
|
931
|
-
|
|
932
|
-
|
|
614
|
+
Avoid unwrapping in normal application flows. `unwrapOr`, `unwrapOrThrow`, `_unsafeUnwrap`, and
|
|
615
|
+
`_unsafeUnwrapErr` are best reserved for deliberate final boundaries or tests.
|
|
933
616
|
|
|
934
|
-
|
|
935
|
-
email: string,
|
|
936
|
-
): StrictResult<User, InvalidEmailError | UserAlreadyExistsError> =>
|
|
937
|
-
safeTry(function* () {
|
|
938
|
-
const validEmail = yield* validateEmail(email)
|
|
939
|
-
const availableEmail = yield* ensureUserDoesNotExist(validEmail)
|
|
617
|
+
## Strict Results At Production Boundaries
|
|
940
618
|
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
```
|
|
619
|
+
`Result<T, E>` intentionally allows any failure type. Strings, enums, and small objects can be useful
|
|
620
|
+
inside narrow local workflows.
|
|
944
621
|
|
|
945
|
-
|
|
622
|
+
For service, HTTP, job, queue, CLI, and integration boundaries, prefer the Error-only aliases:
|
|
946
623
|
|
|
947
624
|
```ts
|
|
948
|
-
|
|
949
|
-
email: string,
|
|
950
|
-
): StrictResultAsync<User, InvalidEmailError | UserAlreadyExistsError | DatabaseError> =>
|
|
951
|
-
safeTry(async function* () {
|
|
952
|
-
const validEmail = yield* validateEmail(email)
|
|
953
|
-
const availableEmail = yield* ensureUserDoesNotExist(validEmail)
|
|
954
|
-
|
|
955
|
-
return insertUserAsync(availableEmail)
|
|
956
|
-
})
|
|
957
|
-
```
|
|
958
|
-
|
|
959
|
-
The object form can map unexpected thrown errors while leaving yielded `Err` values unchanged:
|
|
625
|
+
import type { StrictResult, StrictResultAsync } from 'resultar'
|
|
960
626
|
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
const raw = yield* readConfigFile()
|
|
627
|
+
const validateAccount = (
|
|
628
|
+
input: string,
|
|
629
|
+
): StrictResult<string, InvalidEmailError> => normalizeEmail(input)
|
|
965
630
|
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
})
|
|
631
|
+
const provisionAccount = (
|
|
632
|
+
input: string,
|
|
633
|
+
): StrictResultAsync<Account, CreateAccountAsyncError> => createAccountAsync(input)
|
|
970
634
|
```
|
|
971
635
|
|
|
972
|
-
|
|
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.
|
|
639
|
+
|
|
640
|
+
## Prevent Ignored Results
|
|
973
641
|
|
|
974
|
-
|
|
975
|
-
|
|
642
|
+
Result values are useful only when callers handle them. Install `resultar-check` with a project-local
|
|
643
|
+
TypeScript 7+:
|
|
976
644
|
|
|
977
645
|
```sh
|
|
978
|
-
pnpm add -D resultar-check typescript
|
|
646
|
+
pnpm add -D resultar-check "typescript@>=7"
|
|
979
647
|
```
|
|
980
648
|
|
|
981
|
-
|
|
649
|
+
Use the CLI as the authoritative local and CI check:
|
|
982
650
|
|
|
983
651
|
```json
|
|
984
652
|
{
|
|
@@ -988,150 +656,89 @@ Add a check script:
|
|
|
988
656
|
}
|
|
989
657
|
```
|
|
990
658
|
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
These fail in the default `must-use` mode:
|
|
994
|
-
|
|
995
|
-
```ts
|
|
996
|
-
saveUser(input)
|
|
997
|
-
const result = saveUser(input)
|
|
998
|
-
```
|
|
999
|
-
|
|
1000
|
-
These are intentional:
|
|
1001
|
-
|
|
1002
|
-
```ts
|
|
1003
|
-
return saveUser(input)
|
|
1004
|
-
void saveUser(input)
|
|
1005
|
-
saveUser(input).match(handleSaved, handleError)
|
|
1006
|
-
```
|
|
1007
|
-
|
|
1008
|
-
The default mode is neverthrow-style `must-use`: it also reports assigned `Result` values that are
|
|
1009
|
-
only passed around and never consumed with `match`, `unwrapOr`, `_unsafeUnwrap`, `isOk`, `isErr`,
|
|
1010
|
-
returned, or explicitly discarded. Use `--mode direct` for the lower-noise expression-only check.
|
|
1011
|
-
|
|
1012
|
-
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:
|
|
1013
661
|
|
|
1014
662
|
```json
|
|
1015
663
|
{
|
|
1016
664
|
"$schema": "./node_modules/resultar-check/schema.json",
|
|
1017
665
|
"compilerOptions": {
|
|
1018
|
-
"plugins": [
|
|
666
|
+
"plugins": [
|
|
667
|
+
{
|
|
668
|
+
"name": "resultar-check",
|
|
669
|
+
"noDiscard": "error"
|
|
670
|
+
}
|
|
671
|
+
]
|
|
1019
672
|
}
|
|
1020
673
|
}
|
|
1021
674
|
```
|
|
1022
675
|
|
|
1023
|
-
|
|
1024
|
-
|
|
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
|
|
1025
685
|
|
|
1026
|
-
|
|
1027
|
-
|
|
686
|
+
Resultar's Fetch-first request packages return `ResultAsync` values for request creation, network,
|
|
687
|
+
HTTP status, JSON parsing, validation, and retry failures.
|
|
1028
688
|
|
|
1029
|
-
|
|
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.
|
|
1030
698
|
|
|
1031
699
|
## API Decision Guide
|
|
1032
700
|
|
|
1033
701
|
| Need | Use |
|
|
1034
702
|
| --- | --- |
|
|
1035
|
-
| Create
|
|
1036
|
-
| Create
|
|
1037
|
-
|
|
|
1038
|
-
|
|
|
1039
|
-
|
|
|
1040
|
-
|
|
|
1041
|
-
|
|
|
1042
|
-
|
|
|
1043
|
-
|
|
|
1044
|
-
|
|
|
1045
|
-
|
|
|
1046
|
-
|
|
|
1047
|
-
|
|
|
1048
|
-
|
|
|
1049
|
-
|
|
|
1050
|
-
|
|
|
1051
|
-
|
|
|
1052
|
-
|
|
|
1053
|
-
|
|
|
1054
|
-
|
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
- No schedule engine in core.
|
|
1075
|
-
- No config, cache, request resolver, resource, fiber, or service runtime.
|
|
1076
|
-
- No raw `Error | T` conversion API.
|
|
1077
|
-
|
|
1078
|
-
## Version 3 Notes
|
|
1079
|
-
|
|
1080
|
-
`3.0.0` is a semver-major release:
|
|
1081
|
-
|
|
1082
|
-
- `no-discard` tooling moved out of the runtime package and into dedicated packages.
|
|
1083
|
-
- TypeScript peer support is now `>=6.0.0`.
|
|
1084
|
-
- Deprecated APIs are not part of the current API:
|
|
1085
|
-
|
|
1086
|
-
- `Result#finally`
|
|
1087
|
-
- `ResultAsync#finally`
|
|
1088
|
-
- `ResultAsync#safeUnwrap`
|
|
1089
|
-
- `safeTryAsync`
|
|
1090
|
-
|
|
1091
|
-
Use `log`, `toDisposable`, `toAsyncDisposable`, and `safeTry(async function* () { ... })` instead.
|
|
1092
|
-
`Result#safeUnwrap()` remains available for compatibility, but new code should prefer
|
|
1093
|
-
`yield* result`.
|
|
1094
|
-
|
|
1095
|
-
`tryCatch` and `tryCatchAsync` remain exported as backward-compatible aliases. Prefer `tryResult`
|
|
1096
|
-
and `tryResultAsync` in new code and docs.
|
|
1097
|
-
|
|
1098
|
-
## Documentation
|
|
1099
|
-
|
|
1100
|
-
This README is the runtime package entry point. For a full guide with larger examples and the
|
|
1101
|
-
complete API map, see the repository documentation.
|
|
1102
|
-
|
|
1103
|
-
More focused material:
|
|
1104
|
-
|
|
1105
|
-
- [Full guide](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md)
|
|
1106
|
-
- [Type-safe error handling article, English](https://github.com/inaiat/resultar/blob/main/articles/en/type-safe.md)
|
|
1107
|
-
- [Artigo sobre tratamento de erros type-safe, Portuguese](https://github.com/inaiat/resultar/blob/main/articles/pt/type-safe.md)
|
|
1108
|
-
- [resultar-check package guide](https://github.com/inaiat/resultar/blob/main/packages/resultar-check/README.md)
|
|
1109
|
-
|
|
1110
|
-
## Repository
|
|
1111
|
-
|
|
1112
|
-
This repository is a pnpm workspace:
|
|
1113
|
-
|
|
1114
|
-
- `packages/resultar`: Resultar runtime package.
|
|
1115
|
-
- `packages/resultar-check`: TypeScript 7 plus Resultar validation.
|
|
1116
|
-
- `packages/resultar-lint`: deprecated compatibility wrapper.
|
|
1117
|
-
- `packages/resultar-tsgo`: deprecated compatibility wrapper for older installs.
|
|
1118
|
-
- `benchmarks`: benchmark package.
|
|
1119
|
-
- `examples/resultar`: runnable core Resultar cookbook.
|
|
1120
|
-
- `examples/lint`: TypeScript 7 `resultar-check` smoke example.
|
|
1121
|
-
|
|
1122
|
-
Common commands:
|
|
1123
|
-
|
|
1124
|
-
```sh
|
|
1125
|
-
pnpm install
|
|
1126
|
-
pnpm run fmt:check
|
|
1127
|
-
pnpm run check
|
|
1128
|
-
pnpm test
|
|
1129
|
-
pnpm run test:cov
|
|
1130
|
-
pnpm run build
|
|
1131
|
-
pnpm run check:full
|
|
1132
|
-
pnpm run test:examples
|
|
1133
|
-
pnpm run bench
|
|
1134
|
-
```
|
|
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).
|
|
1135
742
|
|
|
1136
743
|
## License
|
|
1137
744
|
|