resultar 1.4.5 → 2.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +618 -1398
- package/dist/index.d.ts +77 -1
- package/dist/index.js +126 -36
- package/dist/index.js.map +1 -1
- package/dist/no-discard.d.ts +18 -0
- package/dist/no-discard.js +124 -0
- package/dist/no-discard.js.map +1 -0
- package/dist/result-async-CdlVdXgX.js +698 -0
- package/dist/result-async-CdlVdXgX.js.map +1 -0
- package/dist/result-async-CxGBicja.d.ts +439 -0
- package/package.json +76 -78
- package/dist/chunk-C3IJPSX2.js +0 -718
- package/dist/chunk-C3IJPSX2.js.map +0 -1
- package/dist/chunk-VBI3T5VS.js +0 -19
- package/dist/chunk-VBI3T5VS.js.map +0 -1
- package/dist/error.cjs +0 -43
- package/dist/error.cjs.map +0 -1
- package/dist/error.d.cts +0 -1
- package/dist/error.d.ts +0 -1
- package/dist/error.js +0 -7
- package/dist/error.js.map +0 -1
- package/dist/index.cjs +0 -764
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -1
- package/dist/result-async.cjs +0 -745
- package/dist/result-async.cjs.map +0 -1
- package/dist/result-async.d.cts +0 -1
- package/dist/result-async.d.ts +0 -1
- package/dist/result-async.js +0 -26
- package/dist/result-async.js.map +0 -1
- package/dist/result.cjs +0 -737
- package/dist/result.cjs.map +0 -1
- package/dist/result.d.cts +0 -1
- package/dist/result.d.ts +0 -1
- package/dist/result.js +0 -22
- package/dist/result.js.map +0 -1
- package/dist/utils.cjs +0 -722
- package/dist/utils.cjs.map +0 -1
- package/dist/utils.d.cts +0 -612
- package/dist/utils.d.ts +0 -612
- package/dist/utils.js +0 -14
- package/dist/utils.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,1657 +1,877 @@
|
|
|
1
1
|
# Resultar
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Resultar is a TypeScript library for robust, type-safe error handling without throwing exceptions. It's a fork of the excellent [neverthrow](https://github.com/supermacro/neverthrow) library, adding features for more flexibility in error handling
|
|
6
|
-
|
|
7
|
-
In the spirit of open-source development, **resultar** builds upon the solid foundation laid by neverthrow, extending its functionality to better suit specific needs. It introduces additional methods and functions such as tap, safeTryAsync, and fromThrowableAsync and finally providing more flexibility and control in error handling.
|
|
8
|
-
|
|
9
|
-
## Description
|
|
10
|
-
|
|
11
|
-
Encode failure into your program.
|
|
12
|
-
|
|
13
|
-
This package contains a `Result` type that represents either success (`Ok`) or failure (`Err`).
|
|
14
|
-
|
|
15
|
-
For asynchronous tasks, `resultar` offers a `ResultAsync` class which wraps a `Promise<Result<T, E>>` and gives you the same level of expressivity and control as a regular `Result<T, E>`.
|
|
16
|
-
|
|
17
|
-
`ResultAsync` is `thenable` meaning it **behaves exactly like a native `Promise<Result>`** ... except you have access to the same methods that `Result` provides without having to `await` or `.then` the promise!
|
|
18
|
-
|
|
19
|
-
### Check out [the wiki](https://github.com/inaiat/resultar/wiki) for examples and best practices.
|
|
20
|
-
|
|
21
|
-
<div id="toc"></div>
|
|
22
|
-
|
|
23
|
-
## Table Of Contents
|
|
24
|
-
|
|
25
|
-
* [Installation](#installation)
|
|
26
|
-
* [Top-Level API](#top-level-api)
|
|
27
|
-
* [API Documentation](#api-documentation)
|
|
28
|
-
+ [Synchronous API (`Result`)](#synchronous-api-result)
|
|
29
|
-
- [`ok`](#ok)
|
|
30
|
-
- [`err`](#err)
|
|
31
|
-
- [`unit`](#unit)
|
|
32
|
-
- [`Result.isOk` (method)](#resultisok-method)
|
|
33
|
-
- [`Result.isErr` (method)](#resultiserr-method)
|
|
34
|
-
- [`Result.map` (method)](#resultmap-method)
|
|
35
|
-
- [`Result.mapErr` (method)](#resultmaperr-method)
|
|
36
|
-
- [`Result.unwrapOr` (method)](#resultunwrapor-method)
|
|
37
|
-
- [`Result.andThen` (method)](#resultandthen-method)
|
|
38
|
-
- [`Result.asyncAndThen` (method)](#resultasyncandthen-method)
|
|
39
|
-
- [`Result.orElse` (method)](#resultorelse-method)
|
|
40
|
-
- [`Result.tap` (method)](#resulttap-method)
|
|
41
|
-
- [`Result.tapError` (method)](#resulttaperror-method)
|
|
42
|
-
- [`Result.log` (method)](#resultlog-method)
|
|
43
|
-
- [`Result.finally` (method)](#resultfinally-method)
|
|
44
|
-
- [`Result.match` (method)](#resultmatch-method)
|
|
45
|
-
- [`Result.asyncMap` (method)](#resultasyncmap-method)
|
|
46
|
-
- [`Result.fromThrowable` (static class method)](#resultfromthrowable-static-class-method)
|
|
47
|
-
- [`Result.combine` (static class method)](#resultcombine-static-class-method)
|
|
48
|
-
- [`Result.combineWithAllErrors` (static class method)](#resultcombinewithallerrors-static-class-method)
|
|
49
|
-
- [`Result.safeUnwrap()`](#resultsafeunwrap)
|
|
50
|
-
- [`Result.tryCatch` (static class method)](#resulttrycatch-static-class-method)
|
|
51
|
-
+ [Asynchronous API (`ResultAsync`)](#asynchronous-api-resultasync)
|
|
52
|
-
- [`okAsync`](#okasync)
|
|
53
|
-
- [`errAsync`](#errasync)
|
|
54
|
-
- [`unitAsync`](#unitasync)
|
|
55
|
-
- [`ResultAsync.fromPromise` (static class method)](#resultasyncfrompromise-static-class-method)
|
|
56
|
-
- [`ResultAsync.fromSafePromise` (static class method)](#resultasyncfromsafepromise-static-class-method)
|
|
57
|
-
- [`ResultAsync.fromThrowable` (static class method)](#resultasyncfromthrowable-static-class-method)
|
|
58
|
-
- [`ResultAsync.map` (method)](#resultasyncmap-method)
|
|
59
|
-
- [`ResultAsync.mapErr` (method)](#resultasyncmaperr-method)
|
|
60
|
-
- [`ResultAsync.unwrapOr` (method)](#resultasyncunwrapor-method)
|
|
61
|
-
- [`ResultAsync.andThen` (method)](#resultasyncandthen-method)
|
|
62
|
-
- [`ResultAsync.orElse` (method)](#resultasyncorelse-method)
|
|
63
|
-
- [`ResultAsync.tap` (method)](#resultasynctap-method)
|
|
64
|
-
- [`ResultAsync.tapError` (method)](#resultasynctaperror-method)
|
|
65
|
-
- [`ResultAsync.finally` (method)](#resultasyncfinally-method)
|
|
66
|
-
- [`ResultAsync.match` (method)](#resultasyncmatch-method)
|
|
67
|
-
- [`ResultAsync.combine` (static class method)](#resultasynccombine-static-class-method)
|
|
68
|
-
- [`ResultAsync.combineWithAllErrors` (static class method)](#resultasynccombinewithallerrors-static-class-method)
|
|
69
|
-
- [`ResultAsync.safeUnwrap()`](#resultasyncsafeunwrap)
|
|
70
|
-
- [`ResultAsync.tryCatch` (static class method)](#resultasynctrycatch-static-class-method)
|
|
71
|
-
+ [Utilities](#utilities)
|
|
72
|
-
- [`fromThrowable`](#fromthrowable)
|
|
73
|
-
- [`fromThrowableAsync`](#fromthrowableasync)
|
|
74
|
-
- [`fromPromise`](#frompromise)
|
|
75
|
-
- [`fromSafePromise`](#fromsafepromise)
|
|
76
|
-
- [`safeTry`](#safetry)
|
|
77
|
-
- [`tryCatch`](#trycatch)
|
|
78
|
-
- [`tryCatchAsync`](#trycatchasync)
|
|
79
|
-
+ [Testing](#testing)
|
|
80
|
-
* [A note on the Package Name](#a-note-on-the-package-name)
|
|
81
|
-
|
|
82
|
-
## Installation
|
|
3
|
+
Typed `Result` primitives for TypeScript, with ergonomic tagged errors.
|
|
83
4
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
## Top-Level API
|
|
89
|
-
|
|
90
|
-
`resultar` exposes the following:
|
|
91
|
-
|
|
92
|
-
- `ok` convenience function to create an `Ok` variant of `Result`
|
|
93
|
-
- `err` convenience function to create an `Err` variant of `Result`
|
|
94
|
-
- `Ok` class and type
|
|
95
|
-
- `Err` class and type
|
|
96
|
-
- `Result` Type as well as namespace / object from which to call [`Result.fromThrowable`](#resultfromthrowable-static-class-method), [Result.combine](#resultcombine-static-class-method).
|
|
97
|
-
- `ResultAsync` class
|
|
98
|
-
- `okAsync` convenience function to create a `ResultAsync` containing an `Ok` type `Result`
|
|
99
|
-
- `errAsync` convenience function to create a `ResultAsync` containing an `Err` type `Result`
|
|
100
|
-
|
|
101
|
-
```typescript
|
|
102
|
-
import {
|
|
103
|
-
ok,
|
|
104
|
-
Ok,
|
|
105
|
-
err,
|
|
106
|
-
Err,
|
|
107
|
-
Result,
|
|
108
|
-
okAsync,
|
|
109
|
-
errAsync,
|
|
110
|
-
ResultAsync,
|
|
111
|
-
fromThrowable,
|
|
112
|
-
fromPromise,
|
|
113
|
-
fromSafePromise,
|
|
114
|
-
safeTry,
|
|
115
|
-
} from 'resultar'
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
---
|
|
119
|
-
|
|
120
|
-
**Check out the [wiki](https://github.com/inaiat/resultar/wiki) for help on how to make the most of `resultar`.**
|
|
121
|
-
|
|
122
|
-
---
|
|
123
|
-
|
|
124
|
-
## API Documentation
|
|
125
|
-
|
|
126
|
-
### Synchronous API (`Result`)
|
|
127
|
-
|
|
128
|
-
#### `ok`
|
|
129
|
-
|
|
130
|
-
Constructs an `Ok` variant of `Result`
|
|
131
|
-
|
|
132
|
-
**Signature:**
|
|
133
|
-
|
|
134
|
-
```typescript
|
|
135
|
-
ok<T, E>(value: T): Ok<T, E> { ... }
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
**Example:**
|
|
139
|
-
|
|
140
|
-
```typescript
|
|
141
|
-
import { ok } from 'resultar'
|
|
142
|
-
|
|
143
|
-
const myResult = ok({ myData: 'test' }) // instance of `Ok`
|
|
144
|
-
|
|
145
|
-
myResult.isOk() // true
|
|
146
|
-
myResult.isErr() // false
|
|
147
|
-
```
|
|
5
|
+
Resultar began as an initial fork of `neverthrow`. The v2 direction keeps the explicit `Result`
|
|
6
|
+
wrapper model while evolving toward Resultar-specific tagged errors, strict service-boundary types,
|
|
7
|
+
and TypeScript 6-ready ESM packaging.
|
|
148
8
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
---
|
|
152
|
-
|
|
153
|
-
#### `err`
|
|
154
|
-
|
|
155
|
-
Constructs an `Err` variant of `Result`
|
|
156
|
-
|
|
157
|
-
**Signature:**
|
|
158
|
-
|
|
159
|
-
```typescript
|
|
160
|
-
err<T, E>(error: E): Err<T, E> { ... }
|
|
161
|
-
```
|
|
9
|
+
Resultar models fallible work explicitly:
|
|
162
10
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const myResult = err('Oh noooo') // instance of `Err`
|
|
169
|
-
|
|
170
|
-
myResult.isOk() // false
|
|
171
|
-
myResult.isErr() // true
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
[⬆️ Back to top](#toc)
|
|
175
|
-
---
|
|
176
|
-
|
|
177
|
-
#### `unit`
|
|
178
|
-
|
|
179
|
-
Returns a `Result` with a `value` of `undefined`.
|
|
180
|
-
|
|
181
|
-
**Signature:**
|
|
182
|
-
```typescript
|
|
183
|
-
unit(): Result<undefined, never> { ... }
|
|
11
|
+
```ts
|
|
12
|
+
Result<T, E>
|
|
13
|
+
ResultAsync<T, E>
|
|
14
|
+
StrictResult<T, E extends Error>
|
|
15
|
+
StrictResultAsync<T, E extends Error>
|
|
184
16
|
```
|
|
185
|
-
[⬆️ Back to top](#toc)
|
|
186
|
-
---
|
|
187
17
|
|
|
188
|
-
|
|
18
|
+
Use `Result` when an operation can fail synchronously, `ResultAsync` when it returns a promise, and
|
|
19
|
+
tagged errors when you want strongly typed `Error` classes with stable tags and metadata.
|
|
20
|
+
Prefer `StrictResult` and `StrictResultAsync` at service boundaries so expected failures are real
|
|
21
|
+
`Error` instances with `message`, `cause`, stack traces, and structured metadata.
|
|
189
22
|
|
|
190
|
-
|
|
23
|
+
## Requirements
|
|
191
24
|
|
|
192
|
-
|
|
25
|
+
- ESM only
|
|
26
|
+
- Node.js 24+
|
|
27
|
+
- TypeScript 6-ready
|
|
193
28
|
|
|
194
|
-
```
|
|
195
|
-
|
|
29
|
+
```ts
|
|
30
|
+
import { ok, err, createTaggedError } from 'resultar'
|
|
196
31
|
```
|
|
197
32
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
---
|
|
201
|
-
|
|
202
|
-
#### `Result.isErr` (method)
|
|
33
|
+
CommonJS `require('resultar')` is not exported.
|
|
203
34
|
|
|
204
|
-
|
|
35
|
+
## Install
|
|
205
36
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
```typescript
|
|
209
|
-
isErr(): boolean { ... }
|
|
37
|
+
```sh
|
|
38
|
+
pnpm add resultar
|
|
210
39
|
```
|
|
211
40
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
---
|
|
215
|
-
|
|
216
|
-
#### `Result.map` (method)
|
|
217
|
-
|
|
218
|
-
Maps a `Result<T, E>` to `Result<U, E>` by applying a function to a contained `Ok` value, leaving an `Err` value untouched.
|
|
219
|
-
|
|
220
|
-
This function can be used to compose the results of two functions.
|
|
221
|
-
|
|
222
|
-
**Signature:**
|
|
223
|
-
|
|
224
|
-
```typescript
|
|
225
|
-
class Result<T, E> {
|
|
226
|
-
map<U>(callback: (value: T) => U): Result<U, E> { ... }
|
|
227
|
-
}
|
|
41
|
+
```sh
|
|
42
|
+
npm install resultar
|
|
228
43
|
```
|
|
229
44
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
```typescript
|
|
233
|
-
import { getLines } from 'imaginary-parser'
|
|
234
|
-
// ^ assume getLines has the following signature:
|
|
235
|
-
// getLines(str: string): Result<Array<string>, Error>
|
|
236
|
-
|
|
237
|
-
// since the formatting is deemed correct by `getLines`
|
|
238
|
-
// then it means that `linesResult` is an Ok
|
|
239
|
-
// containing an Array of strings for each line of code
|
|
240
|
-
const linesResult = getLines('1\n2\n3\n4\n')
|
|
45
|
+
## V2 Deprecation Notice
|
|
241
46
|
|
|
242
|
-
|
|
243
|
-
const newResult = linesResult.map(
|
|
244
|
-
(arr: Array<string>) => arr.map(parseInt)
|
|
245
|
-
)
|
|
246
|
-
|
|
247
|
-
newResult.isOk() // true
|
|
248
|
-
```
|
|
47
|
+
The `2.0.0` line removes deprecated APIs before stable release:
|
|
249
48
|
|
|
250
|
-
|
|
49
|
+
| Removed API | Use instead |
|
|
50
|
+
| --- | --- |
|
|
51
|
+
| `Result#finally(fn)` | `Result#log(fn)` for immediate side effects, or `Result#toDisposable(fn)` for Node.js `using` cleanup |
|
|
52
|
+
| `ResultAsync#finally(fn)` | `ResultAsync#log(fn)` for immediate side effects, or `ResultAsync#toAsyncDisposable(fn)` for Node.js `await using` cleanup |
|
|
53
|
+
| `ResultAsync#safeUnwrap()` | `yield* resultAsync` inside `safeTry(async function* () { ... })` |
|
|
54
|
+
| `safeTryAsync(...)` | `safeTry(async function* () { ... })` |
|
|
251
55
|
|
|
252
|
-
|
|
56
|
+
`Result#safeUnwrap()` remains available for compatibility, but new code should prefer `yield* result`.
|
|
253
57
|
|
|
254
|
-
|
|
58
|
+
## Philosophy
|
|
255
59
|
|
|
256
|
-
|
|
60
|
+
Resultar is a small explicit-result library, not an application runtime or an Effect-style framework.
|
|
61
|
+
It focuses on three things:
|
|
257
62
|
|
|
258
|
-
|
|
63
|
+
- Make expected failures visible in function signatures.
|
|
64
|
+
- Compose sync and async fallible work without `try/catch` control-flow blocks.
|
|
65
|
+
- Keep expected application failures observable by modeling them as real typed `Error` values.
|
|
259
66
|
|
|
260
|
-
|
|
67
|
+
The core model stays intentionally simple:
|
|
261
68
|
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
}
|
|
69
|
+
```ts
|
|
70
|
+
Result<T, E>
|
|
71
|
+
ResultAsync<T, E>
|
|
266
72
|
```
|
|
267
73
|
|
|
268
|
-
|
|
74
|
+
Use generic `Result<T, E>` when local code benefits from lightweight strings, enums, or domain
|
|
75
|
+
objects. Use `StrictResult<T, E extends Error>` and `StrictResultAsync<T, E extends Error>` when a
|
|
76
|
+
failure can cross a module, service, HTTP, job, or integration boundary.
|
|
269
77
|
|
|
270
|
-
|
|
271
|
-
import { parseHeaders } from 'imaginary-http-parser'
|
|
272
|
-
// imagine that parseHeaders has the following signature:
|
|
273
|
-
// parseHeaders(raw: string): Result<SomeKeyValueMap, ParseError>
|
|
78
|
+
## Recommended Production Style
|
|
274
79
|
|
|
275
|
-
|
|
80
|
+
For backend and application code, prefer this stack:
|
|
276
81
|
|
|
277
|
-
|
|
82
|
+
1. Define expected domain/application failures with `createTaggedError`.
|
|
83
|
+
2. Return them through `StrictResult` or `StrictResultAsync`.
|
|
84
|
+
3. Compose with `map`, `andThen`, `asyncAndThen`, `orElse`, `catchTag`, and `catchTags`.
|
|
85
|
+
4. Map outcomes at boundaries with `matchTags` or `matchTagsPartial`.
|
|
86
|
+
5. Use `tap`, `tapError`, and `log` only for best-effort side effects.
|
|
278
87
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
error: parseError
|
|
282
|
-
})
|
|
283
|
-
})
|
|
284
|
-
|
|
285
|
-
parseResult.isErr() // true
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
[⬆️ Back to top](#toc)
|
|
289
|
-
|
|
290
|
-
---
|
|
291
|
-
|
|
292
|
-
#### `Result.unwrapOr` (method)
|
|
88
|
+
```ts
|
|
89
|
+
import type { StrictResult } from 'resultar'
|
|
293
90
|
|
|
294
|
-
|
|
91
|
+
import { createTaggedError, ok } from 'resultar'
|
|
295
92
|
|
|
296
|
-
|
|
93
|
+
class UserAlreadyExistsError extends createTaggedError({
|
|
94
|
+
name: 'UserAlreadyExistsError',
|
|
95
|
+
message: 'User $email already exists',
|
|
96
|
+
}) {}
|
|
297
97
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
}
|
|
98
|
+
const ensureUserDoesNotExist = (
|
|
99
|
+
email: string,
|
|
100
|
+
): StrictResult<string, UserAlreadyExistsError> =>
|
|
101
|
+
email === 'taken@example.com' ? UserAlreadyExistsError.err({ email }) : ok(email)
|
|
302
102
|
```
|
|
303
103
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
```typescript
|
|
307
|
-
const myResult = err('Oh noooo')
|
|
104
|
+
Avoid using strings or loose objects for production-facing expected failures:
|
|
308
105
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
106
|
+
```ts
|
|
107
|
+
// Local-only style. Avoid at service boundaries.
|
|
108
|
+
type ParseError = 'InvalidPort'
|
|
312
109
|
```
|
|
313
110
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
---
|
|
317
|
-
|
|
318
|
-
#### `Result.andThen` (method)
|
|
319
|
-
|
|
320
|
-
Same idea as `map` above. Except you must return a new `Result`.
|
|
111
|
+
### No-Discard Check
|
|
321
112
|
|
|
322
|
-
|
|
113
|
+
Resultar ships a type-aware check for discarded `Result` and `ResultAsync` values:
|
|
323
114
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
```typescript
|
|
331
|
-
class Result<T, E> {
|
|
332
|
-
// Note that the latest version lets you return distinct errors as well.
|
|
333
|
-
// If the error types (E and F) are the same (like `string | string`)
|
|
334
|
-
// then they will be merged into one type (`string`)
|
|
335
|
-
andThen<U, F>(
|
|
336
|
-
callback: (value: T) => Result<U, F>
|
|
337
|
-
): Result<U, E | F> { ... }
|
|
115
|
+
```json
|
|
116
|
+
{
|
|
117
|
+
"scripts": {
|
|
118
|
+
"lint:resultar": "resultar-no-discard --project tsconfig.json"
|
|
119
|
+
}
|
|
338
120
|
}
|
|
339
121
|
```
|
|
340
122
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
```typescript
|
|
344
|
-
import { err, ok } from 'resultar'
|
|
345
|
-
|
|
346
|
-
const sq = (n: number): Result<number, number> => ok(n ** 2)
|
|
123
|
+
It fails code that ignores a returned result:
|
|
347
124
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
.andThen(sq) // Ok(16)
|
|
351
|
-
|
|
352
|
-
ok(2)
|
|
353
|
-
.andThen(sq)
|
|
354
|
-
.andThen(err) // Err(4)
|
|
355
|
-
|
|
356
|
-
ok(2)
|
|
357
|
-
.andThen(err)
|
|
358
|
-
.andThen(sq) // Err(2)
|
|
359
|
-
|
|
360
|
-
err(3)
|
|
361
|
-
.andThen(sq)
|
|
362
|
-
.andThen(sq) // Err(3)
|
|
363
|
-
```
|
|
364
|
-
|
|
365
|
-
**Example 2: Flattening Nested Results**
|
|
366
|
-
|
|
367
|
-
```typescript
|
|
368
|
-
// It's common to have nested Results
|
|
369
|
-
const nested = ok(ok(1234))
|
|
370
|
-
|
|
371
|
-
// notNested is a Ok(1234)
|
|
372
|
-
const notNested = nested.andThen((innerResult) => innerResult)
|
|
125
|
+
```ts
|
|
126
|
+
saveUser(input)
|
|
373
127
|
```
|
|
374
128
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
---
|
|
378
|
-
|
|
379
|
-
#### `Result.asyncAndThen` (method)
|
|
380
|
-
|
|
381
|
-
Same idea as [`andThen` above](#resultandthen-method), except you must return a new `ResultAsync`.
|
|
382
|
-
|
|
383
|
-
The returned value will be a `ResultAsync`.
|
|
384
|
-
|
|
385
|
-
**Signature:**
|
|
129
|
+
Handle the result, return it, or mark an intentional discard with `void`:
|
|
386
130
|
|
|
387
|
-
```
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
): ResultAsync<U, E | F> { ... }
|
|
392
|
-
}
|
|
131
|
+
```ts
|
|
132
|
+
const result = saveUser(input)
|
|
133
|
+
return saveUser(input)
|
|
134
|
+
void saveUser(input)
|
|
393
135
|
```
|
|
394
136
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
---
|
|
398
|
-
|
|
399
|
-
#### `Result.orElse` (method)
|
|
400
|
-
|
|
401
|
-
Takes an `Err` value and maps it to a `Result<T, SomeNewType>`. This is useful for error recovery.
|
|
402
|
-
|
|
403
|
-
**Signature:**
|
|
137
|
+
For Oxlint users, run it next to Oxlint because this rule needs TypeScript return-type information:
|
|
404
138
|
|
|
405
|
-
```
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
139
|
+
```json
|
|
140
|
+
{
|
|
141
|
+
"scripts": {
|
|
142
|
+
"lint": "oxlint && resultar-no-discard --project tsconfig.json"
|
|
143
|
+
}
|
|
410
144
|
}
|
|
411
145
|
```
|
|
412
146
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
```typescript
|
|
416
|
-
enum DatabaseError {
|
|
417
|
-
PoolExhausted = 'PoolExhausted',
|
|
418
|
-
NotFound = 'NotFound',
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
const dbQueryResult: Result<string, DatabaseError> = err(DatabaseError.NotFound)
|
|
422
|
-
|
|
423
|
-
const updatedQueryResult = dbQueryResult.orElse((dbError) =>
|
|
424
|
-
dbError === DatabaseError.NotFound
|
|
425
|
-
? ok('User does not exist') // error recovery branch: ok() must be called with a value of type string
|
|
426
|
-
//
|
|
427
|
-
//
|
|
428
|
-
// err() can be called with a value of any new type that you want
|
|
429
|
-
// it could also be called with the same error value
|
|
430
|
-
//
|
|
431
|
-
// err(dbError)
|
|
432
|
-
: err(500)
|
|
433
|
-
)
|
|
434
|
-
```
|
|
147
|
+
## API Decision Guide
|
|
435
148
|
|
|
436
|
-
|
|
149
|
+
Use `map` when the `Ok` value changes and the transform cannot fail.
|
|
437
150
|
|
|
438
|
-
|
|
151
|
+
Use `andThen` when the next step can fail and returns another `Result`. Use `asyncAndThen` when a
|
|
152
|
+
sync `Result` continues into `ResultAsync`.
|
|
439
153
|
|
|
440
|
-
|
|
154
|
+
Use `mapErr` when the error value changes. Use `orElse` when an error should recover into another
|
|
155
|
+
`Result`.
|
|
441
156
|
|
|
442
|
-
|
|
157
|
+
Use `catchTag` or `catchTags` for local recovery from tagged errors. `catchTags` is intentionally
|
|
158
|
+
partial: unhandled tags stay in the error channel.
|
|
443
159
|
|
|
444
|
-
|
|
160
|
+
Use `match` for simple boundaries. Use `matchTags` when every tagged error must be handled. Use
|
|
161
|
+
`matchTagsPartial` when only selected tagged errors need custom handling and the rest should go to a
|
|
162
|
+
fallback.
|
|
445
163
|
|
|
446
|
-
|
|
164
|
+
Use `fromPromise(promise, toError)` when you already have a promise. Use
|
|
165
|
+
`tryCatchAsync(() => promiseFactory(), toError)` when creating the promise can throw synchronously.
|
|
447
166
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
okCallback: (value: T) => A,
|
|
452
|
-
errorCallback: (error: E) => A
|
|
453
|
-
): A => { ... }
|
|
454
|
-
}
|
|
455
|
-
```
|
|
167
|
+
Use `tryCatch` / `tryCatchAsync` at the edge of uncontrolled code: JSON parsing, third-party
|
|
168
|
+
libraries, I/O, network calls, and other APIs that throw or reject. Keep your own domain functions
|
|
169
|
+
returning `Result` values instead of throwing expected failures.
|
|
456
170
|
|
|
457
|
-
|
|
458
|
-
The differences between `match` and chaining `map` and `mapErr` are that:
|
|
459
|
-
- with `match` both functions must have the same return type `A`
|
|
460
|
-
- `match` unwraps the `Result<T, E>` into an `A` (the match functions' return type)
|
|
461
|
-
- This makes no difference if you are performing side effects only
|
|
462
|
-
|
|
463
|
-
**Example:**
|
|
464
|
-
|
|
465
|
-
```typescript
|
|
466
|
-
// map/mapErr api
|
|
467
|
-
// note that you DON'T have to append mapErr
|
|
468
|
-
// after map which means that you are not required to do
|
|
469
|
-
// error handling
|
|
470
|
-
computationThatMightFail().map(console.log).mapErr(console.error)
|
|
471
|
-
|
|
472
|
-
// match api
|
|
473
|
-
// works exactly the same as above since both callbacks
|
|
474
|
-
// only perform side effects,
|
|
475
|
-
// except, now you HAVE to do error handling :)
|
|
476
|
-
computationThatMightFail().match(console.log, console.error)
|
|
477
|
-
|
|
478
|
-
// Returning values
|
|
479
|
-
const attempt = computationThatMightFail()
|
|
480
|
-
.map((str) => str.toUpperCase())
|
|
481
|
-
.mapErr((err) => `Error: ${err}`)
|
|
482
|
-
// `attempt` is of type `Result<string, string>`
|
|
483
|
-
|
|
484
|
-
const answer = computationThatMightFail().match(
|
|
485
|
-
(str) => str.toUpperCase(),
|
|
486
|
-
(err) => `Error: ${err}`
|
|
487
|
-
)
|
|
488
|
-
// `answer` is of type `string`
|
|
489
|
-
```
|
|
171
|
+
## Quick Start
|
|
490
172
|
|
|
491
|
-
If you don't use the error parameter in your match callback then `match` is equivalent to chaining `map` with `unwrapOr`:
|
|
492
173
|
```ts
|
|
493
|
-
|
|
494
|
-
(str) => str.toUpperCase(),
|
|
495
|
-
() => 'ComputationError'
|
|
496
|
-
)
|
|
497
|
-
// `answer` is of type `string`
|
|
498
|
-
|
|
499
|
-
const answer = computationThatMightFail()
|
|
500
|
-
.map((str) => str.toUpperCase())
|
|
501
|
-
.unwrapOr('ComputationError')
|
|
502
|
-
```
|
|
174
|
+
import type { StrictResult } from 'resultar'
|
|
503
175
|
|
|
176
|
+
import { createTaggedError, ok } from 'resultar'
|
|
504
177
|
|
|
505
|
-
|
|
178
|
+
class InvalidPortError extends createTaggedError({
|
|
179
|
+
name: 'InvalidPortError',
|
|
180
|
+
message: 'Invalid port $value',
|
|
181
|
+
}) {}
|
|
506
182
|
|
|
507
|
-
|
|
183
|
+
const parsePort = (value: string): StrictResult<number, InvalidPortError> => {
|
|
184
|
+
const port = Number(value)
|
|
508
185
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
- the mapping function must return a `Promise`
|
|
514
|
-
- asyncMap returns a `ResultAsync`
|
|
515
|
-
|
|
516
|
-
You can then chain the result of `asyncMap` using the `ResultAsync` apis (like `map`, `mapErr`, `andThen`, etc.)
|
|
517
|
-
|
|
518
|
-
**Signature:**
|
|
519
|
-
|
|
520
|
-
```typescript
|
|
521
|
-
class Result<T, E> {
|
|
522
|
-
asyncMap<U>(
|
|
523
|
-
callback: (value: T) => Promise<U>
|
|
524
|
-
): ResultAsync<U, E> { ... }
|
|
186
|
+
return Number.isInteger(port) && port > 0
|
|
187
|
+
? ok(port)
|
|
188
|
+
: InvalidPortError.err({ value })
|
|
525
189
|
}
|
|
526
|
-
```
|
|
527
|
-
|
|
528
|
-
**Example:**
|
|
529
|
-
|
|
530
|
-
```typescript
|
|
531
|
-
import { parseHeaders } from 'imaginary-http-parser'
|
|
532
|
-
// imagine that parseHeaders has the following signature:
|
|
533
|
-
// parseHeaders(raw: string): Result<SomeKeyValueMap, ParseError>
|
|
534
|
-
|
|
535
|
-
const asyncRes = parseHeaders(rawHeader)
|
|
536
|
-
.map(headerKvMap => headerKvMap.Authorization)
|
|
537
|
-
.asyncMap(findUserInDatabase)
|
|
538
|
-
```
|
|
539
|
-
|
|
540
|
-
Note that in the above example if `parseHeaders` returns an `Err` then `.map` and `.asyncMap` will not be invoked, and `asyncRes` variable will resolve to an `Err` when turned into a `Result` using `await` or `.then()`.
|
|
541
|
-
|
|
542
|
-
[⬆️ Back to top](#toc)
|
|
543
|
-
|
|
544
|
-
---
|
|
545
|
-
|
|
546
|
-
#### `Result.fromThrowable` (static class method)
|
|
547
|
-
|
|
548
|
-
> Although Result is not an actual JS class, the way that `fromThrowable` has been implemented requires that you call `fromThrowable` as though it were a static method on `Result`. See examples below.
|
|
549
|
-
|
|
550
|
-
The JavaScript community has agreed on the convention of throwing exceptions.
|
|
551
|
-
As such, when interfacing with third party libraries it's imperative that you
|
|
552
|
-
wrap third-party code in try / catch blocks.
|
|
553
|
-
|
|
554
|
-
This function will create a new function that returns an `Err` when the original
|
|
555
|
-
function throws.
|
|
556
|
-
|
|
557
|
-
It is not possible to know the types of the errors thrown in the original
|
|
558
|
-
function, therefore it is recommended to use the second argument `errorFn` to
|
|
559
|
-
map what is thrown to a known type.
|
|
560
|
-
|
|
561
|
-
**Example**:
|
|
562
190
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
const toParseError = (): ParseError => ({ message: "Parse Error" })
|
|
568
|
-
|
|
569
|
-
const safeJsonParse = Result.fromThrowable(JSON.parse, toParseError)
|
|
570
|
-
|
|
571
|
-
// the function can now be used safely, if the function throws, the result will be an Err
|
|
572
|
-
const res = safeJsonParse("{");
|
|
191
|
+
const message = parsePort('3000').match(
|
|
192
|
+
(port) => `Listening on ${port}`,
|
|
193
|
+
(error) => error.message,
|
|
194
|
+
)
|
|
573
195
|
```
|
|
574
196
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
---
|
|
578
|
-
|
|
579
|
-
#### `Result.combine` (static class method)
|
|
580
|
-
|
|
581
|
-
> Although Result is not an actual JS class, the way that `combine` has been implemented requires that you call `combine` as though it were a static method on `Result`. See examples below.
|
|
582
|
-
|
|
583
|
-
Combine lists of `Result`s.
|
|
584
|
-
|
|
585
|
-
If you're familiar with `Promise.all`, the combine function works conceptually the same.
|
|
586
|
-
|
|
587
|
-
**`combine` works on both heterogeneous and homogeneous lists**. This means that you can have lists that contain different kinds of `Result`s and still be able to combine them. Note that you cannot combine lists that contain both `Result`s **and** `ResultAsync`s.
|
|
588
|
-
|
|
589
|
-
The combine function takes a list of results and returns a single result. If all the results in the list are `Ok`, then the return value will be a `Ok` containing a list of all the individual `Ok` values.
|
|
590
|
-
|
|
591
|
-
If just one of the results in the list is an `Err` then the combine function returns that Err value (it short circuits and returns the first Err that it finds).
|
|
592
|
-
|
|
593
|
-
Formally speaking:
|
|
197
|
+
## Strict Results
|
|
594
198
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
function combine<T, E>(resultList: Result<T, E>[]): Result<T[], E>
|
|
199
|
+
`StrictResult<T, E extends Error>` and `StrictResultAsync<T, E extends Error>` are type-only aliases
|
|
200
|
+
for code that should return only `Error`-based failures.
|
|
598
201
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
function combine<T1, T2, T3, E1, E2, E3> => Result<[ T1, T2, T3 ], E1 | E2 | E3>
|
|
602
|
-
function combine<T1, T2, T3, T4, E1, E2, E3, E4> => Result<[ T1, T2, T3, T4 ], E1 | E2 | E3 | E4>
|
|
603
|
-
// ... etc etc ad infinitum
|
|
604
|
-
|
|
605
|
-
```
|
|
606
|
-
|
|
607
|
-
Example:
|
|
608
|
-
```typescript
|
|
609
|
-
const resultList: Result<number, never>[] =
|
|
610
|
-
[ok(1), ok(2)]
|
|
611
|
-
|
|
612
|
-
const combinedList: Result<number[], unknown> =
|
|
613
|
-
Result.combine(resultList)
|
|
614
|
-
```
|
|
202
|
+
```ts
|
|
203
|
+
import type { StrictResult } from 'resultar'
|
|
615
204
|
|
|
616
|
-
|
|
617
|
-
```typescript
|
|
618
|
-
/** @example tuple(1, 2, 3) === [1, 2, 3] // with type [number, number, number] */
|
|
619
|
-
const tuple = <T extends any[]>(...args: T): T => args
|
|
205
|
+
import { createTaggedError, ok } from 'resultar'
|
|
620
206
|
|
|
621
|
-
|
|
622
|
-
|
|
207
|
+
class InvalidEmailError extends createTaggedError({
|
|
208
|
+
name: 'InvalidEmailError',
|
|
209
|
+
message: 'Invalid email $email',
|
|
210
|
+
}) {}
|
|
623
211
|
|
|
624
|
-
const
|
|
625
|
-
|
|
212
|
+
const validateEmail = (email: string): StrictResult<string, InvalidEmailError> =>
|
|
213
|
+
email.includes('@') ? ok(email) : InvalidEmailError.err({ email })
|
|
626
214
|
```
|
|
627
215
|
|
|
628
|
-
|
|
216
|
+
Use generic `Result<T, E>` for small local flows where strings, enums, or lightweight domain objects
|
|
217
|
+
are useful. Prefer `StrictResult` for application and backend service boundaries where logging,
|
|
218
|
+
`cause`, stack traces, and tagged-error matching matter.
|
|
629
219
|
|
|
630
|
-
|
|
220
|
+
## Expected Failure Style
|
|
631
221
|
|
|
632
|
-
|
|
222
|
+
For expected application and domain failures, prefer `createTaggedError` classes and return them
|
|
223
|
+
through `StrictResult` or `StrictResultAsync`.
|
|
633
224
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
Function signature:
|
|
641
|
-
|
|
642
|
-
```typescript
|
|
643
|
-
// homogeneous lists
|
|
644
|
-
function combineWithAllErrors<T, E>(resultList: Result<T, E>[]): Result<T[], E[]>
|
|
645
|
-
|
|
646
|
-
// heterogeneous lists
|
|
647
|
-
function combineWithAllErrors<T1, T2, E1, E2>(resultList: [ Result<T1, E1>, Result<T2, E2> ]): Result<[ T1, T2 ], (E1 | E2)[]>
|
|
648
|
-
function combineWithAllErrors<T1, T2, T3, E1, E2, E3> => Result<[ T1, T2, T3 ], (E1 | E2 | E3)[]>
|
|
649
|
-
function combineWithAllErrors<T1, T2, T3, T4, E1, E2, E3, E4> => Result<[ T1, T2, T3, T4 ], (E1 | E2 | E3 | E4)[]>
|
|
650
|
-
// ... etc etc ad infinitum
|
|
225
|
+
```ts
|
|
226
|
+
class UserAlreadyExistsError extends createTaggedError({
|
|
227
|
+
name: 'UserAlreadyExistsError',
|
|
228
|
+
message: 'User $email already exists',
|
|
229
|
+
}) {}
|
|
651
230
|
```
|
|
652
231
|
|
|
653
|
-
|
|
232
|
+
Plain `Error` subclasses from libraries and runtimes are still valid in strict results when their
|
|
233
|
+
identity is already useful, such as validation, database, abort, or platform errors. Keep
|
|
234
|
+
`Err<string>`, `Err<object>`, and `TaggedEnum` for narrow local flows where stack traces, `cause`,
|
|
235
|
+
structured logging, and boundary matching do not matter.
|
|
654
236
|
|
|
655
|
-
|
|
656
|
-
const resultList: Result<number, string>[] = [
|
|
657
|
-
ok(123),
|
|
658
|
-
err('boooom!'),
|
|
659
|
-
ok(456),
|
|
660
|
-
err('ahhhhh!'),
|
|
661
|
-
]
|
|
237
|
+
## Result
|
|
662
238
|
|
|
663
|
-
|
|
239
|
+
`Result<T, E>` is either `Ok<T>` or `Err<E>`.
|
|
664
240
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
[⬆️ Back to top](#toc)
|
|
669
|
-
|
|
670
|
-
#### `Result.safeUnwrap()`
|
|
671
|
-
|
|
672
|
-
Allows for unwrapping a `Result` or returning an `Err` implicitly, thereby reducing boilerplate.
|
|
241
|
+
```ts
|
|
242
|
+
import type { StrictResult } from 'resultar'
|
|
673
243
|
|
|
674
|
-
|
|
244
|
+
import { createTaggedError, ok } from 'resultar'
|
|
675
245
|
|
|
676
|
-
|
|
246
|
+
class UserNotFoundError extends createTaggedError({
|
|
247
|
+
name: 'UserNotFoundError',
|
|
248
|
+
message: 'User $id not found',
|
|
249
|
+
}) {}
|
|
677
250
|
|
|
678
|
-
|
|
251
|
+
const findUser = (id: string): StrictResult<{ id: string }, UserNotFoundError> =>
|
|
252
|
+
id === 'usr_123' ? ok({ id }) : UserNotFoundError.err({ id })
|
|
679
253
|
|
|
680
|
-
|
|
681
|
-
```typescript
|
|
682
|
-
static tryCatch<T, E>(fn: () => Exclude<T, Promise<any>>, errorFn?: (e: unknown) => E): Result<T, E>
|
|
683
|
-
```
|
|
254
|
+
const result = findUser('usr_123')
|
|
684
255
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
const divide = (a: number, b: number) => {
|
|
688
|
-
if (b === 0) throw new Error("Cannot divide by zero");
|
|
689
|
-
return a / b;
|
|
690
|
-
};
|
|
691
|
-
|
|
692
|
-
// Using destructuring to handle errors procedurally
|
|
693
|
-
const { value, error } = Result.tryCatch(() => divide(10, 2));
|
|
694
|
-
if (error) {
|
|
695
|
-
console.error("Division failed:", error);
|
|
696
|
-
} else {
|
|
697
|
-
console.log("Result:", value); // Result: 5
|
|
256
|
+
if (result.isOk()) {
|
|
257
|
+
result.value.id
|
|
698
258
|
}
|
|
699
259
|
|
|
700
|
-
// With custom error handling
|
|
701
|
-
const result = Result.tryCatch(
|
|
702
|
-
() => divide(10, 0),
|
|
703
|
-
(e) => `Division error: ${e.message}`
|
|
704
|
-
);
|
|
705
260
|
if (result.isErr()) {
|
|
706
|
-
|
|
261
|
+
result.error.message
|
|
707
262
|
}
|
|
708
263
|
```
|
|
709
264
|
|
|
265
|
+
### Chaining
|
|
710
266
|
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
---
|
|
714
|
-
|
|
715
|
-
### Asynchronous API (`ResultAsync`)
|
|
716
|
-
|
|
717
|
-
#### `okAsync`
|
|
718
|
-
|
|
719
|
-
Constructs an `Ok` variant of `ResultAsync`
|
|
720
|
-
|
|
721
|
-
**Signature:**
|
|
722
|
-
|
|
723
|
-
```typescript
|
|
724
|
-
okAsync<T, E>(value: T): ResultAsync<T, E>
|
|
725
|
-
```
|
|
726
|
-
|
|
727
|
-
**Example:**
|
|
728
|
-
|
|
729
|
-
```typescript
|
|
730
|
-
import { okAsync } from 'resultar'
|
|
731
|
-
|
|
732
|
-
const myResultAsync = okAsync({ myData: 'test' }) // instance of `ResultAsync`
|
|
733
|
-
|
|
734
|
-
const myResult = await myResultAsync // instance of `Ok`
|
|
735
|
-
|
|
736
|
-
myResult.isOk() // true
|
|
737
|
-
myResult.isErr() // false
|
|
738
|
-
```
|
|
267
|
+
Use `map` for infallible transforms and `andThen` for fallible transforms.
|
|
739
268
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
---
|
|
743
|
-
|
|
744
|
-
#### `errAsync`
|
|
745
|
-
|
|
746
|
-
Constructs an `Err` variant of `ResultAsync`
|
|
747
|
-
|
|
748
|
-
**Signature:**
|
|
749
|
-
|
|
750
|
-
```typescript
|
|
751
|
-
errAsync<T, E>(error: E): ResultAsync<T, E>
|
|
752
|
-
```
|
|
753
|
-
|
|
754
|
-
**Example:**
|
|
755
|
-
|
|
756
|
-
```typescript
|
|
757
|
-
import { errAsync } from 'resultar'
|
|
758
|
-
|
|
759
|
-
const myResultAsync = errAsync('Oh nooo') // instance of `ResultAsync`
|
|
760
|
-
|
|
761
|
-
const myResult = await myResultAsync // instance of `Err`
|
|
762
|
-
|
|
763
|
-
myResult.isOk() // false
|
|
764
|
-
myResult.isErr() // true
|
|
765
|
-
```
|
|
269
|
+
```ts
|
|
270
|
+
import type { StrictResult } from 'resultar'
|
|
766
271
|
|
|
767
|
-
|
|
272
|
+
import { createTaggedError, ok } from 'resultar'
|
|
768
273
|
|
|
769
|
-
|
|
274
|
+
type User = {
|
|
275
|
+
readonly email: string
|
|
276
|
+
}
|
|
770
277
|
|
|
771
|
-
|
|
278
|
+
class InvalidEmailError extends createTaggedError({
|
|
279
|
+
name: 'InvalidEmailError',
|
|
280
|
+
message: 'Invalid email $email',
|
|
281
|
+
}) {}
|
|
772
282
|
|
|
773
|
-
|
|
283
|
+
class InvalidDomainError extends createTaggedError({
|
|
284
|
+
name: 'InvalidDomainError',
|
|
285
|
+
message: 'Invalid email domain $domain',
|
|
286
|
+
}) {}
|
|
774
287
|
|
|
775
|
-
|
|
288
|
+
const validateEmail = (email: string): StrictResult<string, InvalidEmailError> =>
|
|
289
|
+
email.includes('@') ? ok(email) : InvalidEmailError.err({ email })
|
|
776
290
|
|
|
777
|
-
|
|
778
|
-
|
|
291
|
+
const createUser = (email: string): StrictResult<User, InvalidEmailError> =>
|
|
292
|
+
validateEmail(email).map((validEmail) => ({ email: validEmail }))
|
|
779
293
|
```
|
|
780
294
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
```typescript
|
|
784
|
-
import { errAsync } from 'resultar'
|
|
785
|
-
|
|
786
|
-
const myResultAsync =unitAsync() // instance of `ResultAsync`
|
|
295
|
+
Use `filterOrElse` when a successful value must satisfy an extra predicate:
|
|
787
296
|
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
297
|
+
```ts
|
|
298
|
+
const validateCompanyEmail = (
|
|
299
|
+
email: string,
|
|
300
|
+
): StrictResult<string, InvalidEmailError | InvalidDomainError> =>
|
|
301
|
+
validateEmail(email).filterOrElse(
|
|
302
|
+
(validEmail) => validEmail.endsWith('@company.com'),
|
|
303
|
+
(validEmail) => new InvalidDomainError({ domain: validEmail.split('@')[1] ?? 'unknown' }),
|
|
304
|
+
)
|
|
792
305
|
```
|
|
793
306
|
|
|
794
|
-
|
|
307
|
+
Generic `Result<T, E>` still supports strings, enums, and lightweight objects for narrow local flows.
|
|
308
|
+
For application and backend service boundaries, prefer `StrictResult<T, E extends Error>` with
|
|
309
|
+
`createTaggedError`.
|
|
795
310
|
|
|
796
|
-
|
|
311
|
+
Use `pipe` when you want to apply reusable Result combinators:
|
|
797
312
|
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
**Example**:
|
|
803
|
-
|
|
804
|
-
```typescript
|
|
805
|
-
import { ResultAsync } from 'neverthrow'
|
|
806
|
-
import { insertIntoDb } from 'imaginary-database'
|
|
807
|
-
// insertIntoDb(user: User): Promise<User>
|
|
313
|
+
```ts
|
|
314
|
+
const audit =
|
|
315
|
+
<T, E>(result: Result<T, E>): Result<T, E> =>
|
|
316
|
+
result.tap((value) => console.log('created', value))
|
|
808
317
|
|
|
809
|
-
const
|
|
810
|
-
|
|
318
|
+
const result = createUser('person@example.com').pipe(
|
|
319
|
+
audit,
|
|
320
|
+
(userResult) => userResult.map((user) => user.email),
|
|
321
|
+
)
|
|
811
322
|
```
|
|
812
323
|
|
|
813
|
-
|
|
814
|
-
the result of a function call, because not all functions that return a `Promise` are `async`, and thus they can throw
|
|
815
|
-
errors synchronously rather than returning a rejected `Promise`. For example:
|
|
324
|
+
### Matching
|
|
816
325
|
|
|
817
|
-
|
|
818
|
-
import { readFile } from 'node:fs/promises'
|
|
819
|
-
import { fromThrowableAsync } from 'resultar'
|
|
326
|
+
Use `match` at boundaries.
|
|
820
327
|
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
328
|
+
```ts
|
|
329
|
+
const response = createUser('person@example.com').match(
|
|
330
|
+
(user) => ({ body: user, statusCode: 201 }),
|
|
331
|
+
(error) => ({ body: { error }, statusCode: 400 }),
|
|
824
332
|
)
|
|
825
|
-
const value = await safeFileReader('foo.txt')
|
|
826
|
-
|
|
827
|
-
if (value.isOk()) {
|
|
828
|
-
console.log(value.value.toString())
|
|
829
|
-
}
|
|
830
333
|
```
|
|
831
334
|
|
|
832
|
-
|
|
335
|
+
## ResultAsync
|
|
833
336
|
|
|
834
|
-
|
|
337
|
+
`ResultAsync<T, E>` wraps `Promise<Result<T, E>>` and keeps the same composition style.
|
|
835
338
|
|
|
836
|
-
|
|
339
|
+
```ts
|
|
340
|
+
import type { StrictResultAsync } from 'resultar'
|
|
837
341
|
|
|
838
|
-
|
|
342
|
+
import { createTaggedError, fromPromise } from 'resultar'
|
|
839
343
|
|
|
840
|
-
|
|
344
|
+
type User = {
|
|
345
|
+
readonly id: string
|
|
346
|
+
}
|
|
841
347
|
|
|
348
|
+
class FetchUserError extends createTaggedError({
|
|
349
|
+
name: 'FetchUserError',
|
|
350
|
+
message: 'Failed to fetch user $id',
|
|
351
|
+
}) {}
|
|
842
352
|
|
|
843
|
-
|
|
353
|
+
const fetchUser = (id: string): StrictResultAsync<User, FetchUserError> =>
|
|
354
|
+
fromPromise(
|
|
355
|
+
fetch(`https://example.com/users/${id}`).then((response) => response.json() as Promise<User>),
|
|
356
|
+
(cause) => new FetchUserError({ cause, id }),
|
|
357
|
+
)
|
|
844
358
|
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
ResultAsync.fromPromise<T, E>(
|
|
850
|
-
promise: PromiseLike<T>,
|
|
851
|
-
errorHandler: (unknownError: unknown) => E)
|
|
852
|
-
): ResultAsync<T, E> { ... }
|
|
359
|
+
const label = await fetchUser('usr_123').match(
|
|
360
|
+
(user) => `User ${user.id}`,
|
|
361
|
+
(error) => error.message,
|
|
362
|
+
)
|
|
853
363
|
```
|
|
854
364
|
|
|
855
|
-
|
|
365
|
+
## Tagged Errors
|
|
856
366
|
|
|
857
|
-
|
|
367
|
+
Tagged errors are real `Error` subclasses with:
|
|
858
368
|
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
369
|
+
- `_tag`
|
|
370
|
+
- inferred constructor props from `$variables` in the message template
|
|
371
|
+
- `messageTemplate`
|
|
372
|
+
- `fingerprint`
|
|
373
|
+
- `cause`
|
|
374
|
+
- `.toJSON()`
|
|
375
|
+
- `.findCause(ErrorClass)`
|
|
376
|
+
- static `.is(value)` using `instanceof` for nominal class checks
|
|
377
|
+
- static `.err(props)`
|
|
863
378
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
```
|
|
867
|
-
|
|
868
|
-
[⬆️ Back to top](#toc)
|
|
869
|
-
|
|
870
|
-
---
|
|
871
|
-
|
|
872
|
-
#### `ResultAsync.fromSafePromise` (static class method)
|
|
379
|
+
```ts
|
|
380
|
+
import { createTaggedError } from 'resultar'
|
|
873
381
|
|
|
874
|
-
|
|
382
|
+
class UserNotFoundError extends createTaggedError({
|
|
383
|
+
name: 'UserNotFoundError',
|
|
384
|
+
message: 'User $id not found in $source',
|
|
385
|
+
}) {}
|
|
875
386
|
|
|
876
|
-
|
|
387
|
+
const error = new UserNotFoundError({
|
|
388
|
+
id: 'usr_123',
|
|
389
|
+
source: 'database',
|
|
390
|
+
})
|
|
877
391
|
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
//
|
|
881
|
-
//
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
): ResultAsync<T, E> { ... }
|
|
392
|
+
error instanceof Error // true
|
|
393
|
+
error instanceof UserNotFoundError // true
|
|
394
|
+
error._tag // 'UserNotFoundError'
|
|
395
|
+
error.message // 'User usr_123 not found in database'
|
|
396
|
+
error.id // string | number
|
|
397
|
+
error.source // string | number
|
|
885
398
|
```
|
|
886
399
|
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
```typescript
|
|
890
|
-
import { RouteError } from 'routes/error'
|
|
400
|
+
Missing template props are TypeScript errors:
|
|
891
401
|
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
export const slowDown = <T>(ms: number) => (value: T) =>
|
|
895
|
-
ResultAsync.fromSafePromise<T, RouteError>(
|
|
896
|
-
new Promise((resolve) => {
|
|
897
|
-
setTimeout(() => {
|
|
898
|
-
resolve(value)
|
|
899
|
-
}, ms)
|
|
900
|
-
})
|
|
901
|
-
)
|
|
402
|
+
```ts
|
|
403
|
+
new UserNotFoundError({ id: 'usr_123', source: 'database' })
|
|
902
404
|
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
return createUser(parsed)
|
|
906
|
-
.andThen(slowDown(3000)) // slowdown by 3 seconds
|
|
907
|
-
.andThen(sessionManager.createSession)
|
|
908
|
-
.map(({ sessionToken, admin }) => AppData.init(admin, sessionToken))
|
|
909
|
-
})
|
|
910
|
-
)
|
|
405
|
+
// @ts-expect-error source is required
|
|
406
|
+
new UserNotFoundError({ id: 'usr_123' })
|
|
911
407
|
```
|
|
912
408
|
|
|
409
|
+
Reserved template variables are rejected because they would conflict with `Error` or tagged-error
|
|
410
|
+
metadata:
|
|
913
411
|
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
#### `ResultAsync.map` (method)
|
|
919
|
-
|
|
920
|
-
Maps a `ResultAsync<T, E>` to `ResultAsync<U, E>` by applying a function to a contained `Ok` value, leaving an `Err` value untouched.
|
|
921
|
-
|
|
922
|
-
The applied function can be synchronous or asynchronous (returning a `Promise<U>`) with no impact to the return type.
|
|
923
|
-
|
|
924
|
-
This function can be used to compose the results of two functions.
|
|
925
|
-
|
|
926
|
-
**Signature:**
|
|
927
|
-
|
|
928
|
-
```typescript
|
|
929
|
-
class ResultAsync<T, E> {
|
|
930
|
-
map<U>(
|
|
931
|
-
callback: (value: T) => U | Promise<U>
|
|
932
|
-
): ResultAsync<U, E> { ... }
|
|
933
|
-
}
|
|
412
|
+
```ts
|
|
413
|
+
// @ts-expect-error name is reserved
|
|
414
|
+
createTaggedError({ name: 'BadError', message: 'Bad $name' })
|
|
934
415
|
```
|
|
935
416
|
|
|
936
|
-
|
|
417
|
+
Reserved variables are `_tag`, `name`, `message`, `messageTemplate`, `fingerprint`, `stack`, and
|
|
418
|
+
`cause`.
|
|
937
419
|
|
|
938
|
-
|
|
939
|
-
import { findUsersIn } from 'imaginary-database'
|
|
940
|
-
// ^ assume findUsersIn has the following signature:
|
|
941
|
-
// findUsersIn(country: string): ResultAsync<Array<User>, Error>
|
|
420
|
+
For lightweight tagged domain unions that do not need to extend `Error`, use `TaggedEnum`:
|
|
942
421
|
|
|
943
|
-
|
|
422
|
+
```ts
|
|
423
|
+
import type { TaggedEnum } from 'resultar'
|
|
944
424
|
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
425
|
+
type PaymentError = TaggedEnum<{
|
|
426
|
+
CardDeclined: { readonly code: string }
|
|
427
|
+
InsufficientFunds: { readonly balance: number }
|
|
428
|
+
}>
|
|
948
429
|
|
|
949
|
-
|
|
950
|
-
namesInCanada.then((namesResult: Result<Array<string>, Error>) => {
|
|
951
|
-
if(namesResult.isErr()){
|
|
952
|
-
console.log("Couldn't get the users from the database", namesResult.error)
|
|
953
|
-
}
|
|
954
|
-
else{
|
|
955
|
-
console.log("Users in Canada are named: " + namesResult.value.join(','))
|
|
956
|
-
}
|
|
957
|
-
})
|
|
430
|
+
const error: PaymentError = { _tag: 'CardDeclined', code: 'card_declined' }
|
|
958
431
|
```
|
|
959
432
|
|
|
960
|
-
|
|
433
|
+
## Tagged Errors With Result
|
|
961
434
|
|
|
962
|
-
|
|
435
|
+
Tagged errors are most useful as the `E` side of `StrictResult<T, E>` and
|
|
436
|
+
`StrictResultAsync<T, E>`.
|
|
963
437
|
|
|
964
|
-
|
|
438
|
+
```ts
|
|
439
|
+
import type { StrictResult } from 'resultar'
|
|
965
440
|
|
|
966
|
-
|
|
441
|
+
import { createTaggedError, ok } from 'resultar'
|
|
967
442
|
|
|
968
|
-
|
|
443
|
+
class InvalidEmailError extends createTaggedError({
|
|
444
|
+
name: 'InvalidEmailError',
|
|
445
|
+
message: 'Invalid email $email',
|
|
446
|
+
}) {}
|
|
969
447
|
|
|
970
|
-
|
|
448
|
+
class UserAlreadyExistsError extends createTaggedError({
|
|
449
|
+
name: 'UserAlreadyExistsError',
|
|
450
|
+
message: 'User $email already exists',
|
|
451
|
+
}) {}
|
|
971
452
|
|
|
972
|
-
|
|
453
|
+
class DatabaseError extends createTaggedError({
|
|
454
|
+
name: 'DatabaseError',
|
|
455
|
+
message: 'Database operation $operation failed',
|
|
456
|
+
}) {}
|
|
973
457
|
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
callback: (error: E) => F | Promise<F>
|
|
978
|
-
): ResultAsync<T, F> { ... }
|
|
458
|
+
type User = {
|
|
459
|
+
readonly email: string
|
|
460
|
+
readonly id: string
|
|
979
461
|
}
|
|
980
|
-
```
|
|
981
462
|
|
|
982
|
-
|
|
463
|
+
type CreateUserError = InvalidEmailError | UserAlreadyExistsError | DatabaseError
|
|
983
464
|
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
// ^ assume findUsersIn has the following signature:
|
|
987
|
-
// findUsersIn(country: string): ResultAsync<Array<User>, Error>
|
|
465
|
+
const validateEmail = (email: string): StrictResult<string, InvalidEmailError> =>
|
|
466
|
+
email.includes('@') ? ok(email) : InvalidEmailError.err({ email })
|
|
988
467
|
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
if(
|
|
993
|
-
return
|
|
468
|
+
const ensureUserDoesNotExist = (
|
|
469
|
+
email: string,
|
|
470
|
+
): StrictResult<string, UserAlreadyExistsError | DatabaseError> => {
|
|
471
|
+
if (email === 'taken@example.com') {
|
|
472
|
+
return UserAlreadyExistsError.err({ email })
|
|
994
473
|
}
|
|
995
|
-
// All other errors will be labelled as a system error
|
|
996
|
-
return "System error, please contact an administrator."
|
|
997
|
-
})
|
|
998
|
-
|
|
999
|
-
// usersInCanada is of type ResultAsync<Array<User>, string>
|
|
1000
|
-
|
|
1001
|
-
usersInCanada.then((usersResult: Result<Array<User>, string>) => {
|
|
1002
|
-
if(usersResult.isErr()){
|
|
1003
|
-
res.status(400).json({
|
|
1004
|
-
error: usersResult.error
|
|
1005
|
-
})
|
|
1006
|
-
}
|
|
1007
|
-
else{
|
|
1008
|
-
res.status(200).json({
|
|
1009
|
-
users: usersResult.value
|
|
1010
|
-
})
|
|
1011
|
-
}
|
|
1012
|
-
})
|
|
1013
|
-
```
|
|
1014
474
|
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
---
|
|
1018
|
-
|
|
1019
|
-
#### `ResultAsync.unwrapOr` (method)
|
|
1020
|
-
|
|
1021
|
-
Unwrap the `Ok` value, or return the default if there is an `Err`.
|
|
1022
|
-
Works just like `Result.unwrapOr` but returns a `Promise<T>` instead of `T`.
|
|
1023
|
-
|
|
1024
|
-
**Signature:**
|
|
1025
|
-
|
|
1026
|
-
```typescript
|
|
1027
|
-
class ResultAsync<T, E> {
|
|
1028
|
-
unwrapOr<T>(value: T): Promise<T> { ... }
|
|
475
|
+
return ok(email)
|
|
1029
476
|
}
|
|
1030
|
-
```
|
|
1031
477
|
|
|
1032
|
-
|
|
478
|
+
const insertUser = (email: string): StrictResult<User, DatabaseError> =>
|
|
479
|
+
ok({ email, id: 'usr_123' })
|
|
1033
480
|
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
481
|
+
const createUser = (email: string): StrictResult<User, CreateUserError> =>
|
|
482
|
+
validateEmail(email)
|
|
483
|
+
.andThen(ensureUserDoesNotExist)
|
|
484
|
+
.andThen(insertUser)
|
|
1037
485
|
```
|
|
1038
486
|
|
|
1039
|
-
|
|
487
|
+
`InvalidEmailError.err({ email })` returns `Result<never, InvalidEmailError>`. Because `never` is assignable to every success type, it works naturally in functions returning `Result<string, InvalidEmailError>`.
|
|
1040
488
|
|
|
1041
|
-
|
|
489
|
+
Use `catchTag` when one tagged error can be recovered locally:
|
|
1042
490
|
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
`andThen` is really useful as a tool to flatten a `ResultAsync<ResultAsync<A, E2>, E1>` into a `ResultAsync<A, E2>` (see example below).
|
|
1052
|
-
|
|
1053
|
-
**Signature:**
|
|
1054
|
-
|
|
1055
|
-
```typescript
|
|
1056
|
-
// Note that the latest version (v4.1.0-beta) lets you return distinct errors as well.
|
|
1057
|
-
// If the error types (E and F) are the same (like `string | string`)
|
|
1058
|
-
// then they will be merged into one type (`string`)
|
|
1059
|
-
|
|
1060
|
-
class ResultAsync<T, E> {
|
|
1061
|
-
andThen<U, F>(
|
|
1062
|
-
callback: (value: T) => Result<U, F> | ResultAsync<U, F>
|
|
1063
|
-
): ResultAsync<U, E | F> { ... }
|
|
1064
|
-
}
|
|
491
|
+
```ts
|
|
492
|
+
const result = createUser('bad-email').catchTag('InvalidEmailError', (error) =>
|
|
493
|
+
ok({
|
|
494
|
+
email: error.email,
|
|
495
|
+
id: 'draft_user',
|
|
496
|
+
}),
|
|
497
|
+
)
|
|
1065
498
|
```
|
|
1066
499
|
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
```typescript
|
|
1070
|
-
|
|
1071
|
-
import { validateUser } from 'imaginary-validator'
|
|
1072
|
-
import { insertUser } from 'imaginary-database'
|
|
1073
|
-
import { sendNotification } from 'imaginary-service'
|
|
500
|
+
Use `catchTags` when a small set of tagged errors share local recovery:
|
|
1074
501
|
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
const resAsync = validateUser(user)
|
|
1081
|
-
.andThen(insertUser)
|
|
1082
|
-
.andThen(sendNotification)
|
|
1083
|
-
|
|
1084
|
-
// resAsync is a ResultAsync<void, Error>
|
|
1085
|
-
|
|
1086
|
-
resAsync.then((res: Result<void, Error>) => {
|
|
1087
|
-
if(res.isErr()){
|
|
1088
|
-
console.log("Oops, at least one step failed", res.error)
|
|
1089
|
-
}
|
|
1090
|
-
else{
|
|
1091
|
-
console.log("User has been validated, inserted and notified successfully.")
|
|
1092
|
-
}
|
|
502
|
+
```ts
|
|
503
|
+
const result = createUser('taken@example.com').catchTags({
|
|
504
|
+
InvalidEmailError: (error) => ok({ email: error.email, id: 'draft_user' }),
|
|
505
|
+
UserAlreadyExistsError: (error) => ok({ email: error.email, id: 'existing_user' }),
|
|
1093
506
|
})
|
|
1094
507
|
```
|
|
1095
508
|
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
---
|
|
1099
|
-
#### `Result.tap` (method)
|
|
1100
|
-
Executes a side effect function with the `Ok` value and returns the original `Result`.
|
|
1101
|
-
This method is useful for performing actions that do not modify the `Result` itself, such as logging or updating external state.
|
|
1102
|
-
**Signature:**
|
|
1103
|
-
```typescript
|
|
1104
|
-
class ResultAsync<T, E> {
|
|
1105
|
-
tap(fn: (t: T) => void): Result<T, E> {
|
|
1106
|
-
}
|
|
1107
|
-
```
|
|
1108
|
-
|
|
1109
|
-
**Example:**
|
|
1110
|
-
```typescript
|
|
1111
|
-
import { ok } from "resultar"
|
|
1112
|
-
import { equal } from 'node:assert'
|
|
1113
|
-
|
|
1114
|
-
const foo = ok("foo")
|
|
1115
|
-
const value = foo
|
|
1116
|
-
.tap(console.error) // Prints "foo"
|
|
1117
|
-
.map((value) => value.toUpperCase())
|
|
1118
|
-
.tap(console.error) // Prints "FOO"
|
|
1119
|
-
.match((v)=> v, (error) => "")
|
|
1120
|
-
equal(value, "FOO")
|
|
1121
|
-
```
|
|
1122
|
-
[⬆️ Back to top](#toc)
|
|
1123
|
-
|
|
1124
|
-
---
|
|
1125
|
-
#### `Result.tapError` (method)
|
|
1126
|
-
Executes a side effect function with the `Err` value and returns the original `Result`.
|
|
1127
|
-
This method is useful for performing actions that do not modify the `Result` itself, such as logging or updating external state.
|
|
1128
|
-
**Signature:**
|
|
1129
|
-
```typescript
|
|
1130
|
-
class ResultAsync<T, E> {
|
|
1131
|
-
tapError(fn: (e: E) => void): Result<T, E> {
|
|
1132
|
-
}
|
|
1133
|
-
```
|
|
1134
|
-
|
|
1135
|
-
**Example:**
|
|
1136
|
-
```typescript
|
|
1137
|
-
import { err } from "resultar"
|
|
1138
|
-
import { equal } from 'node:assert'
|
|
1139
|
-
|
|
1140
|
-
const fooError = err("foo")
|
|
1141
|
-
const value = fooError
|
|
1142
|
-
.tapError(console.error) // Prints "foo"
|
|
1143
|
-
.orElse((error) => {
|
|
1144
|
-
return err("bar")
|
|
1145
|
-
})
|
|
1146
|
-
.tapError(console.error) // Prints "bar"
|
|
1147
|
-
.match(()=> "", (error) => error)
|
|
1148
|
-
equal(value, "bar")
|
|
1149
|
-
// mapped.value === 'foo'
|
|
1150
|
-
```
|
|
1151
|
-
[⬆️ Back to top](#toc)
|
|
509
|
+
`catchTags` is intentionally partial: any unhandled error tag stays in the `Err` side. A separate
|
|
510
|
+
`partialCatchTags` API would duplicate this behavior.
|
|
1152
511
|
|
|
1153
|
-
|
|
512
|
+
At the boundary, use `matchTags` for direct success and tagged-error response mapping:
|
|
1154
513
|
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
514
|
+
```ts
|
|
515
|
+
const result = createUser('taken@example.com')
|
|
516
|
+
|
|
517
|
+
const response = result.matchTags(
|
|
518
|
+
(user) => ({
|
|
519
|
+
body: user,
|
|
520
|
+
statusCode: 201,
|
|
521
|
+
}),
|
|
522
|
+
{
|
|
523
|
+
InvalidEmailError: (err) => ({
|
|
524
|
+
body: { code: err._tag, message: err.message },
|
|
525
|
+
statusCode: 400,
|
|
526
|
+
}),
|
|
527
|
+
UserAlreadyExistsError: (err) => ({
|
|
528
|
+
body: { code: err._tag, message: err.message },
|
|
529
|
+
statusCode: 409,
|
|
530
|
+
}),
|
|
531
|
+
DatabaseError: (err) => ({
|
|
532
|
+
body: { code: err._tag, message: err.message },
|
|
533
|
+
statusCode: 500,
|
|
534
|
+
}),
|
|
535
|
+
},
|
|
536
|
+
)
|
|
1165
537
|
```
|
|
1166
538
|
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
const reader = ok(file) // content file is line 01
|
|
539
|
+
If a handler is missing, TypeScript reports it. For partial boundary matching, use
|
|
540
|
+
`matchTagsPartial(okHandler, handlers, fallback)`:
|
|
1171
541
|
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
},
|
|
1184
|
-
|
|
1185
|
-
|
|
542
|
+
```ts
|
|
543
|
+
const response = result.matchTagsPartial(
|
|
544
|
+
(user) => ({
|
|
545
|
+
body: user,
|
|
546
|
+
statusCode: 201,
|
|
547
|
+
}),
|
|
548
|
+
{
|
|
549
|
+
InvalidEmailError: (err) => ({
|
|
550
|
+
body: { code: err._tag, message: err.message },
|
|
551
|
+
statusCode: 400,
|
|
552
|
+
}),
|
|
553
|
+
},
|
|
554
|
+
(err) => ({
|
|
555
|
+
body: { code: 'UnhandledError', message: err.message },
|
|
556
|
+
statusCode: 500,
|
|
557
|
+
}),
|
|
558
|
+
)
|
|
1186
559
|
```
|
|
1187
|
-
[⬆️ Back to top](#toc)
|
|
1188
560
|
|
|
1189
|
-
|
|
561
|
+
Use `catchTags` for pipeline recovery and `matchTagsPartial` for boundary mapping.
|
|
1190
562
|
|
|
1191
|
-
|
|
1192
|
-
Executes a cleanup function wether the is ok or error. This method is usefull to cleanup resources.
|
|
1193
|
-
**Signature:**
|
|
1194
|
-
```typescript
|
|
1195
|
-
class Result<T, E> {
|
|
1196
|
-
finally(f: (value: T, error: E) => void): DisposableResult<T, E> {..}
|
|
1197
|
-
}
|
|
1198
|
-
```
|
|
563
|
+
When the error union includes plain or untagged `Error`, add an `Error` fallback. Use standalone `matchError` when you only have an error value rather than a `Result`.
|
|
1199
564
|
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
const reader = ok(file) // content file is line 01
|
|
565
|
+
```ts
|
|
566
|
+
declare const error: InvalidEmailError | DatabaseError | Error
|
|
1203
567
|
|
|
1204
|
-
const
|
|
1205
|
-
|
|
568
|
+
const message = matchError(error, {
|
|
569
|
+
DatabaseError: (err) => err.message,
|
|
570
|
+
Error: (err) => err.message,
|
|
571
|
+
InvalidEmailError: (err) => err.message,
|
|
1206
572
|
})
|
|
1207
|
-
|
|
1208
|
-
if (resul.isOk()) {
|
|
1209
|
-
console.log(result.value) // print line 01
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
573
|
```
|
|
1213
|
-
[⬆️ Back to top](#toc)
|
|
1214
574
|
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
#### `ResultAsync.orElse` (method)
|
|
1218
|
-
|
|
1219
|
-
Takes an `Err` value and maps it to a `ResultAsync<T, SomeNewType>`. This is useful for error recovery.
|
|
1220
|
-
|
|
1221
|
-
**Signature:**
|
|
1222
|
-
|
|
1223
|
-
```typescript
|
|
1224
|
-
class ResultAsync<T, E> {
|
|
1225
|
-
orElse<A>(
|
|
1226
|
-
callback: (error: E) => Result<T, A> | ResultAsync<T, A>
|
|
1227
|
-
): ResultAsync<T, A> { ... }
|
|
1228
|
-
}
|
|
1229
|
-
```
|
|
575
|
+
## Partial Matching
|
|
1230
576
|
|
|
1231
|
-
|
|
577
|
+
Use `matchErrorPartial` when only some tagged errors need special handling.
|
|
1232
578
|
|
|
1233
|
-
|
|
579
|
+
```ts
|
|
580
|
+
import { matchErrorPartial } from 'resultar'
|
|
1234
581
|
|
|
1235
|
-
|
|
582
|
+
declare const error: InvalidEmailError | UserAlreadyExistsError | DatabaseError
|
|
1236
583
|
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
584
|
+
const message = matchErrorPartial(
|
|
585
|
+
error,
|
|
586
|
+
{
|
|
587
|
+
InvalidEmailError: (err) => `Invalid input: ${err.email}`,
|
|
588
|
+
},
|
|
589
|
+
(err) => err.message,
|
|
590
|
+
)
|
|
1244
591
|
```
|
|
1245
592
|
|
|
1246
|
-
|
|
1247
|
-
See [`Result.tap` (method)](#resulttap-method)
|
|
593
|
+
## Causes
|
|
1248
594
|
|
|
595
|
+
Tagged errors support native `cause` and cause-chain lookup.
|
|
1249
596
|
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
---
|
|
597
|
+
```ts
|
|
598
|
+
import { findCause } from 'resultar'
|
|
1253
599
|
|
|
1254
|
-
|
|
600
|
+
const cause = new Error('connection refused')
|
|
601
|
+
const error = new DatabaseError({
|
|
602
|
+
cause,
|
|
603
|
+
operation: 'insert-user',
|
|
604
|
+
})
|
|
1255
605
|
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
```typescript
|
|
1260
|
-
class ResultAsync<T, E> {
|
|
1261
|
-
tapError(f: (e: E) => void | Promise<void>): ResultAsync<T, E> {...}
|
|
1262
|
-
}
|
|
606
|
+
error.cause // cause
|
|
607
|
+
findCause(error, Error) // cause
|
|
608
|
+
error.findCause(Error) // cause
|
|
1263
609
|
```
|
|
1264
610
|
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
[⬆️ Back to top](#toc)
|
|
611
|
+
Cause lookup is cycle-safe: circular cause chains stop and return `undefined` when no matching cause
|
|
612
|
+
exists.
|
|
1270
613
|
|
|
614
|
+
## Safe Try
|
|
1271
615
|
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
#### `ResultAsync.finally` (method)
|
|
1275
|
-
Executes a cleanup function wether the is ok or error. This method is usefull to cleanup resources for async operations.
|
|
1276
|
-
**Signature:**
|
|
1277
|
-
```typescript
|
|
1278
|
-
class ResultAsync<T, E> {
|
|
1279
|
-
finally(f: (value: T, error: E) => void): DisposableResultAsync<T, E> {...}
|
|
1280
|
-
}
|
|
1281
|
-
```
|
|
616
|
+
`safeTry` lets you unwrap successful results inside a generator and return early on the first error.
|
|
1282
617
|
|
|
1283
|
-
|
|
1284
|
-
```typescript
|
|
1285
|
-
const fileHandle = await fs.open('foo.txt', 'w')
|
|
618
|
+
The shortest form is `yield* result`. `yield* result.safeUnwrap()` is still supported, but usually not necessary.
|
|
1286
619
|
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
.finally(async () => {
|
|
1290
|
-
console.info('Closing file handle')
|
|
1291
|
-
await fileHandle.close()
|
|
1292
|
-
})
|
|
1293
|
-
|
|
1294
|
-
if (result.isOk()) {
|
|
1295
|
-
console.log(result.value) // == 'A new line of text'
|
|
1296
|
-
}
|
|
1297
|
-
|
|
1298
|
-
```
|
|
620
|
+
```ts
|
|
621
|
+
import type { Result, ResultAsync } from 'resultar'
|
|
1299
622
|
|
|
1300
|
-
|
|
623
|
+
import { ok, safeTry } from 'resultar'
|
|
1301
624
|
|
|
1302
|
-
|
|
625
|
+
declare const readConfig: () => Result<{ port: string }, Error>
|
|
626
|
+
declare const parsePort: (value: string) => Result<number, Error>
|
|
627
|
+
declare const persistPort: (port: number) => ResultAsync<void, Error>
|
|
1303
628
|
|
|
1304
|
-
|
|
629
|
+
const loadPort = (): Result<number, Error> =>
|
|
630
|
+
safeTry(function* () {
|
|
631
|
+
const config = yield* readConfig()
|
|
632
|
+
const port = yield* parsePort(config.port)
|
|
1305
633
|
|
|
1306
|
-
|
|
634
|
+
return ok(port)
|
|
635
|
+
})
|
|
1307
636
|
|
|
1308
|
-
|
|
637
|
+
const savePort = (): ResultAsync<number, Error> =>
|
|
638
|
+
safeTry(async function* () {
|
|
639
|
+
const config = yield* readConfig()
|
|
640
|
+
const port = yield* parsePort(config.port)
|
|
1309
641
|
|
|
1310
|
-
|
|
642
|
+
yield* persistPort(port)
|
|
1311
643
|
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
match<A>(
|
|
1315
|
-
okCallback: (value: T) => A,
|
|
1316
|
-
errorCallback: (error: E) => A
|
|
1317
|
-
): Promise<A> => { ... }
|
|
1318
|
-
}
|
|
644
|
+
return ok(port)
|
|
645
|
+
})
|
|
1319
646
|
```
|
|
1320
647
|
|
|
1321
|
-
|
|
648
|
+
## Side Effects
|
|
1322
649
|
|
|
1323
|
-
|
|
650
|
+
Use `tap`, `tapError`, and `log` for best-effort observation. They are not transform methods: use
|
|
651
|
+
`map`, `mapErr`, `andThen`, or `orElse` when the value or error should change.
|
|
1324
652
|
|
|
1325
|
-
|
|
1326
|
-
import { insertUser } from 'imaginary-database'
|
|
653
|
+
`tap(fn)` runs only for `Ok` and returns the original result unchanged.
|
|
1327
654
|
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
// insertUser(user): ResultAsync<User, Error>
|
|
655
|
+
```ts
|
|
656
|
+
import { ok } from 'resultar'
|
|
1331
657
|
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
.match(
|
|
1336
|
-
(user: User) => `User ${user.name} has been successfully created`,
|
|
1337
|
-
(error: Error) => `User could not be created because ${error.message}`
|
|
1338
|
-
)
|
|
658
|
+
const result = ok({ id: 'usr_123' }).tap((user) => {
|
|
659
|
+
console.info('user loaded', user.id)
|
|
660
|
+
})
|
|
1339
661
|
|
|
1340
|
-
//
|
|
662
|
+
// still Result<{ id: string }, never>
|
|
663
|
+
result
|
|
1341
664
|
```
|
|
1342
665
|
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
---
|
|
666
|
+
`tapError(fn)` runs only for `Err` and returns the original result unchanged.
|
|
1346
667
|
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
Combine lists of `ResultAsync`s.
|
|
1350
|
-
|
|
1351
|
-
If you're familiar with `Promise.all`, the combine function works conceptually the same.
|
|
1352
|
-
|
|
1353
|
-
**`combine` works on both heterogeneous and homogeneous lists**. This means that you can have lists that contain different kinds of `ResultAsync`s and still be able to combine them. Note that you cannot combine lists that contain both `Result`s **and** `ResultAsync`s.
|
|
1354
|
-
|
|
1355
|
-
The combine function takes a list of results and returns a single result. If all the results in the list are `Ok`, then the return value will be a `Ok` containing a list of all the individual `Ok` values.
|
|
1356
|
-
|
|
1357
|
-
If just one of the results in the list is an `Err` then the combine function returns that Err value (it short circuits and returns the first Err that it finds).
|
|
1358
|
-
|
|
1359
|
-
Formally speaking:
|
|
1360
|
-
|
|
1361
|
-
```typescript
|
|
1362
|
-
// homogeneous lists
|
|
1363
|
-
function combine<T, E>(resultList: ResultAsync<T, E>[]): ResultAsync<T[], E>
|
|
1364
|
-
|
|
1365
|
-
// heterogeneous lists
|
|
1366
|
-
function combine<T1, T2, E1, E2>(resultList: [ ResultAsync<T1, E1>, ResultAsync<T2, E2> ]): ResultAsync<[ T1, T2 ], E1 | E2>
|
|
1367
|
-
function combine<T1, T2, T3, E1, E2, E3> => ResultAsync<[ T1, T2, T3 ], E1 | E2 | E3>
|
|
1368
|
-
function combine<T1, T2, T3, T4, E1, E2, E3, E4> => ResultAsync<[ T1, T2, T3, T4 ], E1 | E2 | E3 | E4>
|
|
1369
|
-
// ... etc etc ad infinitum
|
|
1370
|
-
|
|
1371
|
-
```
|
|
668
|
+
```ts
|
|
669
|
+
import { err } from 'resultar'
|
|
1372
670
|
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
[okAsync(1), okAsync(2)]
|
|
671
|
+
const result = err(new Error('database unavailable')).tapError((error) => {
|
|
672
|
+
console.error('load failed', error)
|
|
673
|
+
})
|
|
1377
674
|
|
|
1378
|
-
|
|
1379
|
-
|
|
675
|
+
// still Result<never, Error>
|
|
676
|
+
result
|
|
1380
677
|
```
|
|
1381
678
|
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
/** @example tuple(1, 2, 3) === [1, 2, 3] // with type [number, number, number] */
|
|
1385
|
-
const tuple = <T extends any[]>(...args: T): T => args
|
|
679
|
+
`log(fn)` runs for both states. For `Ok`, the callback receives `(value, undefined)`. For `Err`, it
|
|
680
|
+
receives `(undefined, error)`.
|
|
1386
681
|
|
|
1387
|
-
|
|
1388
|
-
|
|
682
|
+
```ts
|
|
683
|
+
const result = findUser('usr_123').log((user, error) => {
|
|
684
|
+
if (error) {
|
|
685
|
+
console.error('find user failed', error)
|
|
686
|
+
return
|
|
687
|
+
}
|
|
1389
688
|
|
|
1390
|
-
|
|
1391
|
-
|
|
689
|
+
console.info('find user succeeded', user)
|
|
690
|
+
})
|
|
1392
691
|
```
|
|
1393
|
-
[⬆️ Back to top](#toc)
|
|
1394
|
-
|
|
1395
|
-
---
|
|
1396
|
-
|
|
1397
|
-
#### `ResultAsync.combineWithAllErrors` (static class method)
|
|
1398
|
-
|
|
1399
|
-
Like `combine` but without short-circuiting. Instead of just the first error value, you get a list of all error values of the input result list.
|
|
1400
|
-
|
|
1401
|
-
If only some results fail, the new combined error list will only contain the error value of the failed results, meaning that there is no guarantee of the length of the new error list.
|
|
1402
692
|
|
|
1403
|
-
|
|
693
|
+
For `ResultAsync`, these callbacks may be synchronous or asynchronous. The original async result is
|
|
694
|
+
still preserved, even if the callback throws or rejects.
|
|
1404
695
|
|
|
1405
|
-
```
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
function combineWithAllErrors<T1, T2, E1, E2>(resultList: [ ResultAsync<T1, E1>, ResultAsync<T2, E2> ]): ResultAsync<[ T1, T2 ], (E1 | E2)[]>
|
|
1411
|
-
function combineWithAllErrors<T1, T2, T3, E1, E2, E3> => ResultAsync<[ T1, T2, T3 ], (E1 | E2 | E3)[]>
|
|
1412
|
-
function combineWithAllErrors<T1, T2, T3, T4, E1, E2, E3, E4> => ResultAsync<[ T1, T2, T3, T4 ], (E1 | E2 | E3 | E4)[]>
|
|
1413
|
-
// ... etc etc ad infinitum
|
|
696
|
+
```ts
|
|
697
|
+
const result = fetchUser('usr_123')
|
|
698
|
+
.tap((user) => metrics.increment('user.found', { id: user.id }))
|
|
699
|
+
.tapError((error) => metrics.increment('user.failed', { message: error.message }))
|
|
700
|
+
.log((user, error) => audit.write({ error, user }))
|
|
1414
701
|
```
|
|
1415
702
|
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
```typescript
|
|
1419
|
-
const resultList: ResultAsync<number, string>[] = [
|
|
1420
|
-
okAsync(123),
|
|
1421
|
-
errAsync('boooom!'),
|
|
1422
|
-
okAsync(456),
|
|
1423
|
-
errAsync('ahhhhh!'),
|
|
1424
|
-
]
|
|
703
|
+
For Node.js 24 cleanup, use `toDisposable(fn)` or `toAsyncDisposable(fn)`. Those APIs defer cleanup
|
|
704
|
+
until the resource is disposed with `using`, `await using`, or the matching symbol method.
|
|
1425
705
|
|
|
1426
|
-
|
|
706
|
+
```ts
|
|
707
|
+
using result = findUser('usr_123').toDisposable((_user, _error) => {
|
|
708
|
+
span.end()
|
|
709
|
+
})
|
|
1427
710
|
|
|
1428
|
-
|
|
711
|
+
await using asyncResult = fetchUser('usr_123').toAsyncDisposable(async (_user, _error) => {
|
|
712
|
+
await span.end()
|
|
713
|
+
})
|
|
1429
714
|
```
|
|
1430
715
|
|
|
1431
|
-
|
|
716
|
+
Callback errors thrown by `tap`, `tapError`, `log`, and disposable cleanup are intentionally ignored
|
|
717
|
+
so observability or cleanup code does not replace the original result. For `ResultAsync`, rejected
|
|
718
|
+
callback promises are ignored too. Put fallible work in `andThen`, `orElse`, `tryCatch`, or
|
|
719
|
+
`tryCatchAsync` when those failures should become part of the result type.
|
|
1432
720
|
|
|
1433
|
-
|
|
721
|
+
## Coming From Other Styles
|
|
1434
722
|
|
|
1435
|
-
|
|
723
|
+
### From try/catch
|
|
1436
724
|
|
|
1437
|
-
|
|
725
|
+
Use `tryCatch` and `tryCatchAsync` only where uncontrolled code can throw. Convert that failure into
|
|
726
|
+
a typed error once, then return `Result` values from your own code.
|
|
1438
727
|
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
**Example:**
|
|
1447
|
-
```typescript
|
|
1448
|
-
const divideAsync = async (a: number, b: number) => {
|
|
1449
|
-
await new Promise(resolve => setTimeout(resolve, 100)); // Simulate async operation
|
|
1450
|
-
if (b === 0) throw new Error("Cannot divide by zero");
|
|
1451
|
-
return a / b;
|
|
1452
|
-
};
|
|
1453
|
-
|
|
1454
|
-
// Using async/await with destructuring
|
|
1455
|
-
const { value, error } = await ResultAsync.tryCatch(divideAsync(10, 2));
|
|
1456
|
-
if (error) {
|
|
1457
|
-
console.error("Async division failed:", error);
|
|
1458
|
-
} else {
|
|
1459
|
-
console.log("Result:", value); // Result: 5
|
|
1460
|
-
}
|
|
1461
|
-
|
|
1462
|
-
// With custom error handling
|
|
1463
|
-
const result = await ResultAsync.tryCatch(
|
|
1464
|
-
divideAsync(10, 0),
|
|
1465
|
-
(e) => `Async division error: ${e.message}`
|
|
1466
|
-
);
|
|
1467
|
-
if (result.isErr()) {
|
|
1468
|
-
console.error(result.error); // Async division error: Cannot divide by zero
|
|
1469
|
-
}
|
|
728
|
+
```ts
|
|
729
|
+
const parseConfig = (input: string): StrictResult<Config, ParseConfigError> =>
|
|
730
|
+
tryCatch(
|
|
731
|
+
() => JSON.parse(input) as Config,
|
|
732
|
+
(cause) => new ParseConfigError({ cause }),
|
|
733
|
+
)
|
|
1470
734
|
```
|
|
1471
735
|
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
###
|
|
736
|
+
### From neverthrow-style wrappers
|
|
737
|
+
|
|
738
|
+
Resultar keeps the wrapper model, but recommends tagged `Error` values for production error
|
|
739
|
+
channels. Use `.value` only after `isOk()` and `.error` only after `isErr()`, or prefer `match` /
|
|
740
|
+
`matchTags` at boundaries.
|
|
741
|
+
|
|
742
|
+
### From raw `T | Error`
|
|
743
|
+
|
|
744
|
+
Raw unions are concise, but they rely on a convention that successful values are never `Error`
|
|
745
|
+
instances. Resultar keeps success and failure structurally separate, so `Ok<Error>` is still a valid
|
|
746
|
+
success value when a domain needs it.
|
|
747
|
+
|
|
748
|
+
### From Effect
|
|
749
|
+
|
|
750
|
+
Resultar does not provide dependency injection, fibers, scopes, schedules, streams, or a runtime. It
|
|
751
|
+
borrows the useful discipline of typed expected errors and composable recovery while staying a small
|
|
752
|
+
library around `Result` and `ResultAsync`.
|
|
753
|
+
|
|
754
|
+
## API
|
|
755
|
+
|
|
756
|
+
### Result Helpers
|
|
757
|
+
|
|
758
|
+
- `ok(value)`
|
|
759
|
+
- `err(error)`
|
|
760
|
+
- `unit()`
|
|
761
|
+
- `tryCatch(fn, errorFn?)`
|
|
762
|
+
- `fromThrowable(fn, errorFn?)`
|
|
763
|
+
- `safeTry(generator)`
|
|
764
|
+
- `StrictResult<T, E extends Error>` type alias for Error-only failure channels
|
|
765
|
+
|
|
766
|
+
### Result Methods
|
|
767
|
+
|
|
768
|
+
- `isOk()`
|
|
769
|
+
- `isErr()`
|
|
770
|
+
- `map(fn)`
|
|
771
|
+
- `mapErr(fn)`
|
|
772
|
+
- `filterOrElse(predicate, onFalse)`
|
|
773
|
+
- `andThen(fn)`
|
|
774
|
+
- `asyncAndThen(fn)`
|
|
775
|
+
- `orElse(fn)`
|
|
776
|
+
- `catchTag(tag, fn)`
|
|
777
|
+
- `catchTags(handlers)`
|
|
778
|
+
- `tap(fn)` run a side effect only when `Ok`; preserves the original result
|
|
779
|
+
- `tapError(fn)` run a side effect only when `Err`; preserves the original result
|
|
780
|
+
- `log(fn)` run a side effect for both states as `(value, error)`; preserves the original result
|
|
781
|
+
- `toDisposable(fn)` create a Node.js `Disposable` result for `using`
|
|
782
|
+
- `match(okFn, errFn)`
|
|
783
|
+
- `matchTags(okFn, handlers)`
|
|
784
|
+
- `matchTagsPartial(okFn, handlers, fallback)`
|
|
785
|
+
- `pipe(fn, ...)`
|
|
786
|
+
- `asyncMap(fn)`
|
|
787
|
+
- `unwrapOr(defaultValue)`
|
|
788
|
+
- `unwrapOrThrow()`
|
|
789
|
+
- `_unsafeUnwrap()`
|
|
790
|
+
- `_unsafeUnwrapErr()`
|
|
791
|
+
- `safeUnwrap()`
|
|
792
|
+
|
|
793
|
+
### Result Static Methods
|
|
794
|
+
|
|
795
|
+
- `Result.ok(value)`
|
|
796
|
+
- `Result.err(error)`
|
|
797
|
+
- `Result.unit()`
|
|
798
|
+
- `Result.tryCatch(fn, errorFn?)`
|
|
799
|
+
- `Result.fromThrowable(fn, errorFn?)`
|
|
800
|
+
- `Result.combine(results)`
|
|
801
|
+
- `Result.combineWithAllErrors(results)`
|
|
802
|
+
|
|
803
|
+
### ResultAsync Helpers
|
|
804
|
+
|
|
805
|
+
- `okAsync(value)`
|
|
806
|
+
- `errAsync(error)`
|
|
807
|
+
- `unitAsync()`
|
|
808
|
+
- `fromPromise(promise, errorFn)`
|
|
809
|
+
- `fromSafePromise(promise)`
|
|
810
|
+
- `fromThrowableAsync(fn, errorFn?)`
|
|
811
|
+
- `tryCatchAsync(promiseOrFn, errorFn?)` catches rejections and synchronous throws from factories
|
|
812
|
+
- `StrictResultAsync<T, E extends Error>` type alias for Error-only async failure channels
|
|
813
|
+
|
|
814
|
+
### ResultAsync Methods
|
|
815
|
+
|
|
816
|
+
- `map(fn)`
|
|
817
|
+
- `mapErr(fn)`
|
|
818
|
+
- `andThen(fn)`
|
|
819
|
+
- `orElse(fn)`
|
|
820
|
+
- `tap(fn)` run a sync or async side effect only when `Ok`; preserves the original result
|
|
821
|
+
- `tapError(fn)` run a sync or async side effect only when `Err`; preserves the original result
|
|
822
|
+
- `log(fn)` run a sync or async side effect for both states as `(value, error)`
|
|
823
|
+
- `toAsyncDisposable(fn)` create a Node.js `AsyncDisposable` result for `await using`
|
|
824
|
+
- `match(okFn, errFn)`
|
|
825
|
+
- `matchTags(okFn, handlers)`
|
|
826
|
+
- `matchTagsPartial(okFn, handlers, fallback)`
|
|
827
|
+
- `unwrapOr(defaultValue)`
|
|
828
|
+
- `unwrapOrThrow()`
|
|
829
|
+
|
|
830
|
+
### Tagged Error Helpers
|
|
831
|
+
|
|
832
|
+
- `createTaggedError(options)`
|
|
833
|
+
- `matchError(error, handlers)`
|
|
834
|
+
- `matchErrorPartial(error, handlers, fallback)`
|
|
835
|
+
- `isError(value)`
|
|
836
|
+
- `findCause(error, ErrorClass)`
|
|
837
|
+
- `TaggedEnum<Members>` type helper
|
|
838
|
+
|
|
839
|
+
## Testing
|
|
840
|
+
|
|
841
|
+
`_unsafeUnwrap()` and `_unsafeUnwrapErr()` are intended for tests.
|
|
1479
842
|
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
Top level export of `Result.fromThrowable`.
|
|
1483
|
-
Please find documentation at [Result.fromThrowable](#resultfromthrowable-static-class-method)
|
|
1484
|
-
|
|
1485
|
-
[⬆️ Back to top](#toc)
|
|
1486
|
-
|
|
1487
|
-
---
|
|
1488
|
-
|
|
1489
|
-
#### `fromThrowableAsync`
|
|
1490
|
-
|
|
1491
|
-
Top level export of `ResultAsync.fromThrowable`.
|
|
1492
|
-
Please find documentation at [ResultAsync.fromThrowable](#resultasyncfromthrowable-static-class-method)
|
|
1493
|
-
|
|
1494
|
-
[⬆️ Back to top](#toc)
|
|
1495
|
-
|
|
1496
|
-
---
|
|
1497
|
-
|
|
1498
|
-
#### `fromPromise`
|
|
1499
|
-
|
|
1500
|
-
Top level export of `ResultAsync.fromPromise`.
|
|
1501
|
-
Please find documentation at [ResultAsync.fromPromise](#resultasyncfrompromise-static-class-method)
|
|
1502
|
-
|
|
1503
|
-
[⬆️ Back to top](#toc)
|
|
1504
|
-
|
|
1505
|
-
---
|
|
1506
|
-
|
|
1507
|
-
#### `tryCatch`
|
|
1508
|
-
|
|
1509
|
-
Top level export of `Result.tryCatch`.
|
|
1510
|
-
Please find documentation at [Result.tryCatch](#resulttrycatch-static-class-method)
|
|
1511
|
-
|
|
1512
|
-
[⬆️ Back to top](#toc)
|
|
1513
|
-
|
|
1514
|
-
---
|
|
1515
|
-
|
|
1516
|
-
#### `tryCatchAsync`
|
|
1517
|
-
|
|
1518
|
-
Top level export of `ResultAsync.tryCatch`.
|
|
1519
|
-
Please find documentation at [ResultAsync.tryCatch](#resultasynctrycatch-static-class-method)
|
|
1520
|
-
|
|
1521
|
-
[⬆️ Back to top](#toc)
|
|
1522
|
-
|
|
1523
|
-
#### `fromSafePromise`
|
|
1524
|
-
|
|
1525
|
-
Top level export of `ResultAsync.fromSafePromise`.
|
|
1526
|
-
Please find documentation at [ResultAsync.fromSafePromise](#resulttasyncfromsafepromise-static-class-method)
|
|
1527
|
-
|
|
1528
|
-
[⬆️ Back to top](#toc)
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
#### `safeTry`
|
|
1532
|
-
|
|
1533
|
-
Used to implicityly return errors and reduce boilerplate.
|
|
1534
|
-
|
|
1535
|
-
Let's say we are writing a function that returns a `Result`, and in that function we call some functions which also return `Result`s and we check those results to see whether we shold keep going or abort. Usually, we will write like the following.
|
|
1536
|
-
```typescript
|
|
1537
|
-
declare function mayFail1(): Result<number, string>;
|
|
1538
|
-
declare function mayFail2(): Result<number, string>;
|
|
1539
|
-
|
|
1540
|
-
function myFunc(): Result<number, string> {
|
|
1541
|
-
// We have to define a constant to hold the result to check and unwrap its value.
|
|
1542
|
-
const result1 = mayFail1();
|
|
1543
|
-
if (result1.isErr()) {
|
|
1544
|
-
return err(`aborted by an error from 1st function, ${result1.error}`);
|
|
1545
|
-
}
|
|
1546
|
-
const value1 = result1.value
|
|
1547
|
-
|
|
1548
|
-
// Again, we need to define a constant and then check and unwrap.
|
|
1549
|
-
const result2 = mayFail2();
|
|
1550
|
-
if (result2.isErr()) {
|
|
1551
|
-
return err(`aborted by an error from 2nd function, ${result2.error}`);
|
|
1552
|
-
}
|
|
1553
|
-
const value2 = result2.value
|
|
1554
|
-
|
|
1555
|
-
// And finally we return what we want to calculate
|
|
1556
|
-
return ok(value1 + value2);
|
|
1557
|
-
}
|
|
1558
|
-
```
|
|
1559
|
-
Basically, we need to define a constant for each result to check whether it's a `Ok` and read its `.value` or `.error`.
|
|
1560
|
-
|
|
1561
|
-
With safeTry, we can state 'Return here if its an `Err`, otherwise unwrap it here and keep going.' in just one expression.
|
|
1562
|
-
```typescript
|
|
1563
|
-
declare function mayFail1(): Result<number, string>;
|
|
1564
|
-
declare function mayFail2(): Result<number, string>;
|
|
1565
|
-
|
|
1566
|
-
function myFunc(): Result<number, string> {
|
|
1567
|
-
return safeTry<number, string>(function*() {
|
|
1568
|
-
return ok(
|
|
1569
|
-
// If the result of mayFail1().mapErr() is an `Err`, the evaluation is
|
|
1570
|
-
// aborted here and the enclosing `safeTry` block is evaluated to that `Err`.
|
|
1571
|
-
// Otherwise, this `(yield* ...)` is evaluated to its `.value`.
|
|
1572
|
-
(yield* mayFail1()
|
|
1573
|
-
.mapErr(e => `aborted by an error from 1st function, ${e}`)
|
|
1574
|
-
.safeUnwrap())
|
|
1575
|
-
+
|
|
1576
|
-
// The same as above.
|
|
1577
|
-
(yield* mayFail2()
|
|
1578
|
-
.mapErr(e => `aborted by an error from 2nd function, ${e}`)
|
|
1579
|
-
.safeUnwrap())
|
|
1580
|
-
)
|
|
1581
|
-
})
|
|
1582
|
-
}
|
|
1583
|
-
```
|
|
843
|
+
```ts
|
|
844
|
+
import { ok } from 'resultar'
|
|
1584
845
|
|
|
1585
|
-
|
|
1586
|
-
* Wrap the entire block in a [generator function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*)
|
|
1587
|
-
* In that block, you can use `yield* <RESULT>` to state 'Return `<RESULT>` if it's an `Err`, otherwise evaluate to its `.value`'
|
|
1588
|
-
* Pass the generator function to `safeTry`
|
|
1589
|
-
|
|
1590
|
-
You can also use [async generator function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function*) to pass an async block to `safeTry`.
|
|
1591
|
-
```typescript
|
|
1592
|
-
// You can use either Promise<Result> or ResultAsync.
|
|
1593
|
-
declare function mayFail1(): Promise<Result<number, string>>;
|
|
1594
|
-
declare function mayFail2(): ResultAsync<number, string>;
|
|
1595
|
-
|
|
1596
|
-
function myFunc(): Promise<Result<number, string>> {
|
|
1597
|
-
return safeTry<number, string>(async function*() {
|
|
1598
|
-
return ok(
|
|
1599
|
-
// You have to await if the expression is Promise<Result>
|
|
1600
|
-
(yield* (await mayFail1())
|
|
1601
|
-
.mapErr(e => `aborted by an error from 1st function, ${e}`)
|
|
1602
|
-
.safeUnwrap())
|
|
1603
|
-
+
|
|
1604
|
-
// You can call `safeUnwrap` directly if its ResultAsync
|
|
1605
|
-
(yield* mayFail2()
|
|
1606
|
-
.mapErr(e => `aborted by an error from 2nd function, ${e}`)
|
|
1607
|
-
.safeUnwrap())
|
|
1608
|
-
)
|
|
1609
|
-
})
|
|
1610
|
-
}
|
|
846
|
+
expect(ok(42)._unsafeUnwrap()).toBe(42)
|
|
1611
847
|
```
|
|
1612
848
|
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
[⬆️ Back to top](#toc)
|
|
1616
|
-
|
|
1617
|
-
---
|
|
1618
|
-
|
|
1619
|
-
### Testing
|
|
849
|
+
By default, thrown unwrap errors omit stack traces for cleaner test output. Pass `withStackTrace` if needed:
|
|
1620
850
|
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
`_unsafeUnwrap` takes a `Result<T, E>` and returns a `T` when the result is an `Ok`, otherwise it throws a custom object.
|
|
1624
|
-
|
|
1625
|
-
`_unsafeUnwrapErr` takes a `Result<T, E>` and returns a `E` when the result is an `Err`, otherwise it throws a custom object.
|
|
1626
|
-
|
|
1627
|
-
That way you can do something like:
|
|
1628
|
-
|
|
1629
|
-
```typescript
|
|
1630
|
-
expect(myResult._unsafeUnwrap()).toBe(someExpectation)
|
|
851
|
+
```ts
|
|
852
|
+
result._unsafeUnwrapErr({ withStackTrace: true })
|
|
1631
853
|
```
|
|
1632
854
|
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
```typescript
|
|
1636
|
-
import { ok } from 'resultar'
|
|
855
|
+
## Development
|
|
1637
856
|
|
|
1638
|
-
|
|
857
|
+
This repository uses Vite+.
|
|
1639
858
|
|
|
1640
|
-
|
|
859
|
+
```sh
|
|
860
|
+
pnpm install
|
|
861
|
+
pnpm run fmt:check
|
|
862
|
+
pnpm run lint
|
|
863
|
+
pnpm test
|
|
864
|
+
pnpm run test:cov
|
|
865
|
+
pnpm run build
|
|
866
|
+
pnpm run check:full
|
|
1641
867
|
```
|
|
1642
868
|
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
```typescript
|
|
1646
|
-
_unsafeUnwrapErr({
|
|
1647
|
-
withStackTrace: true,
|
|
1648
|
-
})
|
|
1649
|
-
|
|
1650
|
-
// ^ Now the error object will have a `.stack` property containing the current stack
|
|
1651
|
-
```
|
|
869
|
+
Package output:
|
|
1652
870
|
|
|
1653
|
-
|
|
871
|
+
- `dist/index.js`
|
|
872
|
+
- `dist/index.js.map`
|
|
873
|
+
- `dist/index.d.ts`
|
|
1654
874
|
|
|
1655
|
-
|
|
875
|
+
## License
|
|
1656
876
|
|
|
1657
|
-
|
|
877
|
+
MIT
|