resultar 1.4.4 → 2.0.0-beta.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 +588 -1417
- package/dist/index.d.ts +532 -1
- package/dist/index.js +841 -36
- package/dist/index.js.map +1 -1
- package/package.json +61 -80
- package/dist/chunk-G4XXW6QA.js +0 -717
- package/dist/chunk-G4XXW6QA.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-lTU9Lum3.d.cts +0 -612
- package/dist/index-lTU9Lum3.d.ts +0 -612
- package/dist/index.cjs +0 -763
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -1
- package/dist/result-async.cjs +0 -744
- 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 -736
- 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 -721
- package/dist/utils.cjs.map +0 -1
- package/dist/utils.d.cts +0 -1
- package/dist/utils.d.ts +0 -1
- package/dist/utils.js +0 -14
- package/dist/utils.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,1657 +1,828 @@
|
|
|
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
|
-
```
|
|
148
|
-
|
|
149
|
-
[⬆️ Back to top](#toc)
|
|
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
|
-
```
|
|
162
|
-
|
|
163
|
-
**Example:**
|
|
164
|
-
|
|
165
|
-
```typescript
|
|
166
|
-
import { err } from 'resultar'
|
|
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> { ... }
|
|
184
|
-
```
|
|
185
|
-
[⬆️ Back to top](#toc)
|
|
186
|
-
---
|
|
187
|
-
|
|
188
|
-
#### `Result.isOk` (method)
|
|
189
|
-
|
|
190
|
-
Returns `true` if the result is an `Ok` variant
|
|
191
|
-
|
|
192
|
-
**Signature:**
|
|
193
|
-
|
|
194
|
-
```typescript
|
|
195
|
-
isOk(): boolean { ... }
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
[⬆️ Back to top](#toc)
|
|
199
|
-
|
|
200
|
-
---
|
|
201
|
-
|
|
202
|
-
#### `Result.isErr` (method)
|
|
203
|
-
|
|
204
|
-
Returns `true` if the result is an `Err` variant
|
|
205
|
-
|
|
206
|
-
**Signature**:
|
|
207
|
-
|
|
208
|
-
```typescript
|
|
209
|
-
isErr(): boolean { ... }
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
[⬆️ Back to top](#toc)
|
|
213
|
-
|
|
214
|
-
---
|
|
215
|
-
|
|
216
|
-
#### `Result.map` (method)
|
|
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.
|
|
217
8
|
|
|
218
|
-
|
|
9
|
+
Resultar models fallible work explicitly:
|
|
219
10
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
class Result<T, E> {
|
|
226
|
-
map<U>(callback: (value: T) => U): Result<U, E> { ... }
|
|
227
|
-
}
|
|
11
|
+
```ts
|
|
12
|
+
Result<T, E>
|
|
13
|
+
ResultAsync<T, E>
|
|
14
|
+
StrictResult<T, E extends Error>
|
|
15
|
+
StrictResultAsync<T, E extends Error>
|
|
228
16
|
```
|
|
229
17
|
|
|
230
|
-
|
|
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.
|
|
231
22
|
|
|
232
|
-
|
|
233
|
-
import { getLines } from 'imaginary-parser'
|
|
234
|
-
// ^ assume getLines has the following signature:
|
|
235
|
-
// getLines(str: string): Result<Array<string>, Error>
|
|
23
|
+
## Requirements
|
|
236
24
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const linesResult = getLines('1\n2\n3\n4\n')
|
|
25
|
+
- ESM only
|
|
26
|
+
- Node.js 24+
|
|
27
|
+
- TypeScript 6-ready
|
|
241
28
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
(arr: Array<string>) => arr.map(parseInt)
|
|
245
|
-
)
|
|
246
|
-
|
|
247
|
-
newResult.isOk() // true
|
|
29
|
+
```ts
|
|
30
|
+
import { ok, err, createTaggedError } from 'resultar'
|
|
248
31
|
```
|
|
249
32
|
|
|
250
|
-
|
|
33
|
+
CommonJS `require('resultar')` is not exported.
|
|
251
34
|
|
|
252
|
-
|
|
35
|
+
## Install
|
|
253
36
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
Maps a `Result<T, E>` to `Result<T, F>` by applying a function to a contained `Err` value, leaving an `Ok` value untouched.
|
|
257
|
-
|
|
258
|
-
This function can be used to pass through a successful result while handling an error.
|
|
259
|
-
|
|
260
|
-
**Signature:**
|
|
261
|
-
|
|
262
|
-
```typescript
|
|
263
|
-
class Result<T, E> {
|
|
264
|
-
mapErr<F>(callback: (error: E) => F): Result<T, F> { ... }
|
|
265
|
-
}
|
|
37
|
+
```sh
|
|
38
|
+
pnpm add resultar
|
|
266
39
|
```
|
|
267
40
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
```typescript
|
|
271
|
-
import { parseHeaders } from 'imaginary-http-parser'
|
|
272
|
-
// imagine that parseHeaders has the following signature:
|
|
273
|
-
// parseHeaders(raw: string): Result<SomeKeyValueMap, ParseError>
|
|
274
|
-
|
|
275
|
-
const rawHeaders = 'nonsensical gibberish and badly formatted stuff'
|
|
276
|
-
|
|
277
|
-
const parseResult = parseHeaders(rawHeaders)
|
|
278
|
-
|
|
279
|
-
parseResult.mapErr(parseError => {
|
|
280
|
-
res.status(400).json({
|
|
281
|
-
error: parseError
|
|
282
|
-
})
|
|
283
|
-
})
|
|
284
|
-
|
|
285
|
-
parseResult.isErr() // true
|
|
41
|
+
```sh
|
|
42
|
+
npm install resultar
|
|
286
43
|
```
|
|
287
44
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
---
|
|
291
|
-
|
|
292
|
-
#### `Result.unwrapOr` (method)
|
|
293
|
-
|
|
294
|
-
Unwrap the `Ok` value, or return the default if there is an `Err`
|
|
295
|
-
|
|
296
|
-
**Signature:**
|
|
297
|
-
|
|
298
|
-
```typescript
|
|
299
|
-
class Result<T, E> {
|
|
300
|
-
unwrapOr<T>(value: T): T { ... }
|
|
301
|
-
}
|
|
302
|
-
```
|
|
45
|
+
## Philosophy
|
|
303
46
|
|
|
304
|
-
|
|
47
|
+
Resultar is a small explicit-result library, not an application runtime or an Effect-style framework.
|
|
48
|
+
It focuses on three things:
|
|
305
49
|
|
|
306
|
-
|
|
307
|
-
|
|
50
|
+
- Make expected failures visible in function signatures.
|
|
51
|
+
- Compose sync and async fallible work without `try/catch` control-flow blocks.
|
|
52
|
+
- Keep expected application failures observable by modeling them as real typed `Error` values.
|
|
308
53
|
|
|
309
|
-
|
|
54
|
+
The core model stays intentionally simple:
|
|
310
55
|
|
|
311
|
-
|
|
56
|
+
```ts
|
|
57
|
+
Result<T, E>
|
|
58
|
+
ResultAsync<T, E>
|
|
312
59
|
```
|
|
313
60
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
#### `Result.andThen` (method)
|
|
319
|
-
|
|
320
|
-
Same idea as `map` above. Except you must return a new `Result`.
|
|
321
|
-
|
|
322
|
-
The returned value will be a `Result`. As of `v4.1.0-beta`, you are able to return distinct error types (see signature below). Prior to `v4.1.0-beta`, the error type could not be distinct.
|
|
323
|
-
|
|
324
|
-
This is useful for when you need to do a subsequent computation using the inner `T` value, but that computation might fail.
|
|
325
|
-
|
|
326
|
-
Additionally, `andThen` is really useful as a tool to flatten a `Result<Result<A, E2>, E1>` into a `Result<A, E2>` (see example below).
|
|
327
|
-
|
|
328
|
-
**Signature:**
|
|
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> { ... }
|
|
338
|
-
}
|
|
339
|
-
```
|
|
61
|
+
Use generic `Result<T, E>` when local code benefits from lightweight strings, enums, or domain
|
|
62
|
+
objects. Use `StrictResult<T, E extends Error>` and `StrictResultAsync<T, E extends Error>` when a
|
|
63
|
+
failure can cross a module, service, HTTP, job, or integration boundary.
|
|
340
64
|
|
|
341
|
-
|
|
65
|
+
## Recommended Production Style
|
|
342
66
|
|
|
343
|
-
|
|
344
|
-
import { err, ok } from 'resultar'
|
|
67
|
+
For backend and application code, prefer this stack:
|
|
345
68
|
|
|
346
|
-
|
|
69
|
+
1. Define expected domain/application failures with `createTaggedError`.
|
|
70
|
+
2. Return them through `StrictResult` or `StrictResultAsync`.
|
|
71
|
+
3. Compose with `map`, `andThen`, `asyncAndThen`, `orElse`, `catchTag`, and `catchTags`.
|
|
72
|
+
4. Map outcomes at boundaries with `matchTags` or `matchTagsPartial`.
|
|
73
|
+
5. Use `tap`, `tapError`, `log`, and `finally` only for best-effort side effects.
|
|
347
74
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
.andThen(sq) // Ok(16)
|
|
75
|
+
```ts
|
|
76
|
+
import type { StrictResult } from 'resultar'
|
|
351
77
|
|
|
352
|
-
ok
|
|
353
|
-
.andThen(sq)
|
|
354
|
-
.andThen(err) // Err(4)
|
|
78
|
+
import { createTaggedError, ok } from 'resultar'
|
|
355
79
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
80
|
+
class UserAlreadyExistsError extends createTaggedError({
|
|
81
|
+
name: 'UserAlreadyExistsError',
|
|
82
|
+
message: 'User $email already exists',
|
|
83
|
+
}) {}
|
|
359
84
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
85
|
+
const ensureUserDoesNotExist = (
|
|
86
|
+
email: string,
|
|
87
|
+
): StrictResult<string, UserAlreadyExistsError> =>
|
|
88
|
+
email === 'taken@example.com' ? UserAlreadyExistsError.err({ email }) : ok(email)
|
|
363
89
|
```
|
|
364
90
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
```typescript
|
|
368
|
-
// It's common to have nested Results
|
|
369
|
-
const nested = ok(ok(1234))
|
|
91
|
+
Avoid using strings or loose objects for production-facing expected failures:
|
|
370
92
|
|
|
371
|
-
|
|
372
|
-
|
|
93
|
+
```ts
|
|
94
|
+
// Local-only style. Avoid at service boundaries.
|
|
95
|
+
type ParseError = 'InvalidPort'
|
|
373
96
|
```
|
|
374
97
|
|
|
375
|
-
|
|
98
|
+
## API Decision Guide
|
|
376
99
|
|
|
377
|
-
|
|
100
|
+
Use `map` when the `Ok` value changes and the transform cannot fail.
|
|
378
101
|
|
|
379
|
-
|
|
102
|
+
Use `andThen` when the next step can fail and returns another `Result`. Use `asyncAndThen` when a
|
|
103
|
+
sync `Result` continues into `ResultAsync`.
|
|
380
104
|
|
|
381
|
-
|
|
105
|
+
Use `mapErr` when the error value changes. Use `orElse` when an error should recover into another
|
|
106
|
+
`Result`.
|
|
382
107
|
|
|
383
|
-
|
|
108
|
+
Use `catchTag` or `catchTags` for local recovery from tagged errors. `catchTags` is intentionally
|
|
109
|
+
partial: unhandled tags stay in the error channel.
|
|
384
110
|
|
|
385
|
-
|
|
111
|
+
Use `match` for simple boundaries. Use `matchTags` when every tagged error must be handled. Use
|
|
112
|
+
`matchTagsPartial` when only selected tagged errors need custom handling and the rest should go to a
|
|
113
|
+
fallback.
|
|
386
114
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
asyncAndThen<U, F>(
|
|
390
|
-
callback: (value: T) => ResultAsync<U, F>
|
|
391
|
-
): ResultAsync<U, E | F> { ... }
|
|
392
|
-
}
|
|
393
|
-
```
|
|
115
|
+
Use `fromPromise(promise, toError)` when you already have a promise. Use
|
|
116
|
+
`tryCatchAsync(() => promiseFactory(), toError)` when creating the promise can throw synchronously.
|
|
394
117
|
|
|
395
|
-
|
|
118
|
+
Use `tryCatch` / `tryCatchAsync` at the edge of uncontrolled code: JSON parsing, third-party
|
|
119
|
+
libraries, I/O, network calls, and other APIs that throw or reject. Keep your own domain functions
|
|
120
|
+
returning `Result` values instead of throwing expected failures.
|
|
396
121
|
|
|
397
|
-
|
|
122
|
+
## Quick Start
|
|
398
123
|
|
|
399
|
-
|
|
124
|
+
```ts
|
|
125
|
+
import type { StrictResult } from 'resultar'
|
|
400
126
|
|
|
401
|
-
|
|
127
|
+
import { createTaggedError, ok } from 'resultar'
|
|
402
128
|
|
|
403
|
-
|
|
129
|
+
class InvalidPortError extends createTaggedError({
|
|
130
|
+
name: 'InvalidPortError',
|
|
131
|
+
message: 'Invalid port $value',
|
|
132
|
+
}) {}
|
|
404
133
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
orElse<A>(
|
|
408
|
-
callback: (error: E) => Result<T, A>
|
|
409
|
-
): Result<T, A> { ... }
|
|
410
|
-
}
|
|
411
|
-
```
|
|
412
|
-
|
|
413
|
-
**Example:**
|
|
134
|
+
const parsePort = (value: string): StrictResult<number, InvalidPortError> => {
|
|
135
|
+
const port = Number(value)
|
|
414
136
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
NotFound = 'NotFound',
|
|
137
|
+
return Number.isInteger(port) && port > 0
|
|
138
|
+
? ok(port)
|
|
139
|
+
: InvalidPortError.err({ value })
|
|
419
140
|
}
|
|
420
141
|
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
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)
|
|
142
|
+
const message = parsePort('3000').match(
|
|
143
|
+
(port) => `Listening on ${port}`,
|
|
144
|
+
(error) => error.message,
|
|
433
145
|
)
|
|
434
146
|
```
|
|
435
147
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
---
|
|
439
|
-
|
|
440
|
-
#### `Result.match` (method)
|
|
441
|
-
|
|
442
|
-
Given 2 functions (one for the `Ok` variant and one for the `Err` variant) execute the function that matches the `Result` variant.
|
|
443
|
-
|
|
444
|
-
Match callbacks do not necessitate to return a `Result`, however you can return a `Result` if you want to.
|
|
148
|
+
## Strict Results
|
|
445
149
|
|
|
446
|
-
|
|
150
|
+
`StrictResult<T, E extends Error>` and `StrictResultAsync<T, E extends Error>` are type-only aliases
|
|
151
|
+
for code that should return only `Error`-based failures.
|
|
447
152
|
|
|
448
|
-
```typescript
|
|
449
|
-
class Result<T, E> {
|
|
450
|
-
match<A>(
|
|
451
|
-
okCallback: (value: T) => A,
|
|
452
|
-
errorCallback: (error: E) => A
|
|
453
|
-
): A => { ... }
|
|
454
|
-
}
|
|
455
|
-
```
|
|
456
|
-
|
|
457
|
-
`match` is like chaining `map` and `mapErr`, with the distinction that with `match` both functions must have the same return type.
|
|
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
|
-
```
|
|
490
|
-
|
|
491
|
-
If you don't use the error parameter in your match callback then `match` is equivalent to chaining `map` with `unwrapOr`:
|
|
492
153
|
```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
|
-
```
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
[⬆️ Back to top](#toc)
|
|
506
|
-
|
|
507
|
-
---
|
|
508
|
-
|
|
509
|
-
#### `Result.asyncMap` (method)
|
|
510
|
-
|
|
511
|
-
Similar to `map` except for two things:
|
|
154
|
+
import type { StrictResult } from 'resultar'
|
|
512
155
|
|
|
513
|
-
|
|
514
|
-
- asyncMap returns a `ResultAsync`
|
|
156
|
+
import { createTaggedError, ok } from 'resultar'
|
|
515
157
|
|
|
516
|
-
|
|
158
|
+
class InvalidEmailError extends createTaggedError({
|
|
159
|
+
name: 'InvalidEmailError',
|
|
160
|
+
message: 'Invalid email $email',
|
|
161
|
+
}) {}
|
|
517
162
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
```typescript
|
|
521
|
-
class Result<T, E> {
|
|
522
|
-
asyncMap<U>(
|
|
523
|
-
callback: (value: T) => Promise<U>
|
|
524
|
-
): ResultAsync<U, E> { ... }
|
|
525
|
-
}
|
|
163
|
+
const validateEmail = (email: string): StrictResult<string, InvalidEmailError> =>
|
|
164
|
+
email.includes('@') ? ok(email) : InvalidEmailError.err({ email })
|
|
526
165
|
```
|
|
527
166
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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.
|
|
167
|
+
Use generic `Result<T, E>` for small local flows where strings, enums, or lightweight domain objects
|
|
168
|
+
are useful. Prefer `StrictResult` for application and backend service boundaries where logging,
|
|
169
|
+
`cause`, stack traces, and tagged-error matching matter.
|
|
560
170
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
```typescript
|
|
564
|
-
import { Result } from 'resultar'
|
|
565
|
-
|
|
566
|
-
type ParseError = { message: string }
|
|
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("{");
|
|
573
|
-
```
|
|
574
|
-
|
|
575
|
-
[⬆️ Back to top](#toc)
|
|
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:
|
|
594
|
-
|
|
595
|
-
```typescript
|
|
596
|
-
// homogeneous lists
|
|
597
|
-
function combine<T, E>(resultList: Result<T, E>[]): Result<T[], E>
|
|
598
|
-
|
|
599
|
-
// heterogeneous lists
|
|
600
|
-
function combine<T1, T2, E1, E2>(resultList: [ Result<T1, E1>, Result<T2, E2> ]): Result<[ T1, T2 ], E1 | E2>
|
|
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
|
-
```
|
|
615
|
-
|
|
616
|
-
Example with tuples:
|
|
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
|
|
620
|
-
|
|
621
|
-
const resultTuple: [Result<string, never>, Result<string, never>] =
|
|
622
|
-
tuple(ok('a'), ok('b'))
|
|
623
|
-
|
|
624
|
-
const combinedTuple: Result<[string, string], unknown> =
|
|
625
|
-
Result.combine(resultTuple)
|
|
626
|
-
```
|
|
171
|
+
## Expected Failure Style
|
|
627
172
|
|
|
628
|
-
|
|
173
|
+
For expected application and domain failures, prefer `createTaggedError` classes and return them
|
|
174
|
+
through `StrictResult` or `StrictResultAsync`.
|
|
629
175
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
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.
|
|
637
|
-
|
|
638
|
-
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.
|
|
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
|
|
176
|
+
```ts
|
|
177
|
+
class UserAlreadyExistsError extends createTaggedError({
|
|
178
|
+
name: 'UserAlreadyExistsError',
|
|
179
|
+
message: 'User $email already exists',
|
|
180
|
+
}) {}
|
|
651
181
|
```
|
|
652
182
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
ok(123),
|
|
658
|
-
err('boooom!'),
|
|
659
|
-
ok(456),
|
|
660
|
-
err('ahhhhh!'),
|
|
661
|
-
]
|
|
662
|
-
|
|
663
|
-
const result = Result.combineWithAllErrors(resultList)
|
|
183
|
+
Plain `Error` subclasses from libraries and runtimes are still valid in strict results when their
|
|
184
|
+
identity is already useful, such as validation, database, abort, or platform errors. Keep
|
|
185
|
+
`Err<string>`, `Err<object>`, and `TaggedEnum` for narrow local flows where stack traces, `cause`,
|
|
186
|
+
structured logging, and boundary matching do not matter.
|
|
664
187
|
|
|
665
|
-
|
|
666
|
-
```
|
|
667
|
-
|
|
668
|
-
[⬆️ Back to top](#toc)
|
|
188
|
+
## Result
|
|
669
189
|
|
|
670
|
-
|
|
190
|
+
`Result<T, E>` is either `Ok<T>` or `Err<E>`.
|
|
671
191
|
|
|
672
|
-
|
|
192
|
+
```ts
|
|
193
|
+
import type { StrictResult } from 'resultar'
|
|
673
194
|
|
|
674
|
-
|
|
195
|
+
import { createTaggedError, ok } from 'resultar'
|
|
675
196
|
|
|
676
|
-
|
|
197
|
+
class UserNotFoundError extends createTaggedError({
|
|
198
|
+
name: 'UserNotFoundError',
|
|
199
|
+
message: 'User $id not found',
|
|
200
|
+
}) {}
|
|
677
201
|
|
|
678
|
-
|
|
202
|
+
const findUser = (id: string): StrictResult<{ id: string }, UserNotFoundError> =>
|
|
203
|
+
id === 'usr_123' ? ok({ id }) : UserNotFoundError.err({ id })
|
|
679
204
|
|
|
680
|
-
|
|
681
|
-
```typescript
|
|
682
|
-
static tryCatch<T, E>(fn: () => Exclude<T, Promise<any>>, errorFn?: (e: unknown) => E): Result<T, E>
|
|
683
|
-
```
|
|
205
|
+
const result = findUser('usr_123')
|
|
684
206
|
|
|
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
|
|
207
|
+
if (result.isOk()) {
|
|
208
|
+
result.value.id
|
|
698
209
|
}
|
|
699
210
|
|
|
700
|
-
// With custom error handling
|
|
701
|
-
const result = Result.tryCatch(
|
|
702
|
-
() => divide(10, 0),
|
|
703
|
-
(e) => `Division error: ${e.message}`
|
|
704
|
-
);
|
|
705
211
|
if (result.isErr()) {
|
|
706
|
-
|
|
212
|
+
result.error.message
|
|
707
213
|
}
|
|
708
214
|
```
|
|
709
215
|
|
|
216
|
+
### Chaining
|
|
710
217
|
|
|
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
|
-
```
|
|
739
|
-
|
|
740
|
-
[⬆️ Back to top](#toc)
|
|
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`
|
|
218
|
+
Use `map` for infallible transforms and `andThen` for fallible transforms.
|
|
762
219
|
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
```
|
|
220
|
+
```ts
|
|
221
|
+
import type { StrictResult } from 'resultar'
|
|
766
222
|
|
|
767
|
-
|
|
223
|
+
import { createTaggedError, ok } from 'resultar'
|
|
768
224
|
|
|
769
|
-
|
|
225
|
+
type User = {
|
|
226
|
+
readonly email: string
|
|
227
|
+
}
|
|
770
228
|
|
|
771
|
-
|
|
229
|
+
class InvalidEmailError extends createTaggedError({
|
|
230
|
+
name: 'InvalidEmailError',
|
|
231
|
+
message: 'Invalid email $email',
|
|
232
|
+
}) {}
|
|
772
233
|
|
|
773
|
-
|
|
234
|
+
class InvalidDomainError extends createTaggedError({
|
|
235
|
+
name: 'InvalidDomainError',
|
|
236
|
+
message: 'Invalid email domain $domain',
|
|
237
|
+
}) {}
|
|
774
238
|
|
|
775
|
-
|
|
239
|
+
const validateEmail = (email: string): StrictResult<string, InvalidEmailError> =>
|
|
240
|
+
email.includes('@') ? ok(email) : InvalidEmailError.err({ email })
|
|
776
241
|
|
|
777
|
-
|
|
778
|
-
|
|
242
|
+
const createUser = (email: string): StrictResult<User, InvalidEmailError> =>
|
|
243
|
+
validateEmail(email).map((validEmail) => ({ email: validEmail }))
|
|
779
244
|
```
|
|
780
245
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
```typescript
|
|
784
|
-
import { errAsync } from 'resultar'
|
|
246
|
+
Use `filterOrElse` when a successful value must satisfy an extra predicate:
|
|
785
247
|
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
248
|
+
```ts
|
|
249
|
+
const validateCompanyEmail = (
|
|
250
|
+
email: string,
|
|
251
|
+
): StrictResult<string, InvalidEmailError | InvalidDomainError> =>
|
|
252
|
+
validateEmail(email).filterOrElse(
|
|
253
|
+
(validEmail) => validEmail.endsWith('@company.com'),
|
|
254
|
+
(validEmail) => new InvalidDomainError({ domain: validEmail.split('@')[1] ?? 'unknown' }),
|
|
255
|
+
)
|
|
792
256
|
```
|
|
793
257
|
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
#### `ResultAsync.fromThrowable` (static class method)
|
|
258
|
+
Generic `Result<T, E>` still supports strings, enums, and lightweight objects for narrow local flows.
|
|
259
|
+
For application and backend service boundaries, prefer `StrictResult<T, E extends Error>` with
|
|
260
|
+
`createTaggedError`.
|
|
799
261
|
|
|
800
|
-
|
|
262
|
+
Use `pipe` when you want to apply reusable Result combinators:
|
|
801
263
|
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
import { insertIntoDb } from 'imaginary-database'
|
|
807
|
-
// insertIntoDb(user: User): Promise<User>
|
|
264
|
+
```ts
|
|
265
|
+
const audit =
|
|
266
|
+
<T, E>(result: Result<T, E>): Result<T, E> =>
|
|
267
|
+
result.tap((value) => console.log('created', value))
|
|
808
268
|
|
|
809
|
-
const
|
|
810
|
-
|
|
269
|
+
const result = createUser('person@example.com').pipe(
|
|
270
|
+
audit,
|
|
271
|
+
(userResult) => userResult.map((user) => user.email),
|
|
272
|
+
)
|
|
811
273
|
```
|
|
812
274
|
|
|
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:
|
|
275
|
+
### Matching
|
|
816
276
|
|
|
817
|
-
|
|
818
|
-
import { readFile } from 'node:fs/promises'
|
|
819
|
-
import { fromThrowableAsync } from 'resultar'
|
|
277
|
+
Use `match` at boundaries.
|
|
820
278
|
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
279
|
+
```ts
|
|
280
|
+
const response = createUser('person@example.com').match(
|
|
281
|
+
(user) => ({ body: user, statusCode: 201 }),
|
|
282
|
+
(error) => ({ body: { error }, statusCode: 400 }),
|
|
824
283
|
)
|
|
825
|
-
const value = await safeFileReader('foo.txt')
|
|
826
|
-
|
|
827
|
-
if (value.isOk()) {
|
|
828
|
-
console.log(value.value.toString())
|
|
829
|
-
}
|
|
830
284
|
```
|
|
831
285
|
|
|
832
|
-
|
|
286
|
+
## ResultAsync
|
|
833
287
|
|
|
834
|
-
|
|
288
|
+
`ResultAsync<T, E>` wraps `Promise<Result<T, E>>` and keeps the same composition style.
|
|
835
289
|
|
|
836
|
-
|
|
290
|
+
```ts
|
|
291
|
+
import type { StrictResultAsync } from 'resultar'
|
|
837
292
|
|
|
838
|
-
|
|
293
|
+
import { createTaggedError, fromPromise } from 'resultar'
|
|
839
294
|
|
|
840
|
-
|
|
295
|
+
type User = {
|
|
296
|
+
readonly id: string
|
|
297
|
+
}
|
|
841
298
|
|
|
299
|
+
class FetchUserError extends createTaggedError({
|
|
300
|
+
name: 'FetchUserError',
|
|
301
|
+
message: 'Failed to fetch user $id',
|
|
302
|
+
}) {}
|
|
842
303
|
|
|
843
|
-
|
|
304
|
+
const fetchUser = (id: string): StrictResultAsync<User, FetchUserError> =>
|
|
305
|
+
fromPromise(
|
|
306
|
+
fetch(`https://example.com/users/${id}`).then((response) => response.json() as Promise<User>),
|
|
307
|
+
(cause) => new FetchUserError({ cause, id }),
|
|
308
|
+
)
|
|
844
309
|
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
ResultAsync.fromPromise<T, E>(
|
|
850
|
-
promise: PromiseLike<T>,
|
|
851
|
-
errorHandler: (unknownError: unknown) => E)
|
|
852
|
-
): ResultAsync<T, E> { ... }
|
|
310
|
+
const label = await fetchUser('usr_123').match(
|
|
311
|
+
(user) => `User ${user.id}`,
|
|
312
|
+
(error) => error.message,
|
|
313
|
+
)
|
|
853
314
|
```
|
|
854
315
|
|
|
855
|
-
|
|
316
|
+
## Tagged Errors
|
|
856
317
|
|
|
857
|
-
|
|
318
|
+
Tagged errors are real `Error` subclasses with:
|
|
858
319
|
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
320
|
+
- `_tag`
|
|
321
|
+
- inferred constructor props from `$variables` in the message template
|
|
322
|
+
- `messageTemplate`
|
|
323
|
+
- `fingerprint`
|
|
324
|
+
- `cause`
|
|
325
|
+
- `.toJSON()`
|
|
326
|
+
- `.findCause(ErrorClass)`
|
|
327
|
+
- static `.is(value)` using `instanceof` for nominal class checks
|
|
328
|
+
- static `.err(props)`
|
|
863
329
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
```
|
|
867
|
-
|
|
868
|
-
[⬆️ Back to top](#toc)
|
|
869
|
-
|
|
870
|
-
---
|
|
871
|
-
|
|
872
|
-
#### `ResultAsync.fromSafePromise` (static class method)
|
|
330
|
+
```ts
|
|
331
|
+
import { createTaggedError } from 'resultar'
|
|
873
332
|
|
|
874
|
-
|
|
333
|
+
class UserNotFoundError extends createTaggedError({
|
|
334
|
+
name: 'UserNotFoundError',
|
|
335
|
+
message: 'User $id not found in $source',
|
|
336
|
+
}) {}
|
|
875
337
|
|
|
876
|
-
|
|
338
|
+
const error = new UserNotFoundError({
|
|
339
|
+
id: 'usr_123',
|
|
340
|
+
source: 'database',
|
|
341
|
+
})
|
|
877
342
|
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
//
|
|
881
|
-
//
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
): ResultAsync<T, E> { ... }
|
|
343
|
+
error instanceof Error // true
|
|
344
|
+
error instanceof UserNotFoundError // true
|
|
345
|
+
error._tag // 'UserNotFoundError'
|
|
346
|
+
error.message // 'User usr_123 not found in database'
|
|
347
|
+
error.id // string | number
|
|
348
|
+
error.source // string | number
|
|
885
349
|
```
|
|
886
350
|
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
```typescript
|
|
890
|
-
import { RouteError } from 'routes/error'
|
|
351
|
+
Missing template props are TypeScript errors:
|
|
891
352
|
|
|
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
|
-
)
|
|
353
|
+
```ts
|
|
354
|
+
new UserNotFoundError({ id: 'usr_123', source: 'database' })
|
|
902
355
|
|
|
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
|
-
)
|
|
356
|
+
// @ts-expect-error source is required
|
|
357
|
+
new UserNotFoundError({ id: 'usr_123' })
|
|
911
358
|
```
|
|
912
359
|
|
|
360
|
+
Reserved template variables are rejected because they would conflict with `Error` or tagged-error
|
|
361
|
+
metadata:
|
|
913
362
|
|
|
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
|
-
}
|
|
363
|
+
```ts
|
|
364
|
+
// @ts-expect-error name is reserved
|
|
365
|
+
createTaggedError({ name: 'BadError', message: 'Bad $name' })
|
|
934
366
|
```
|
|
935
367
|
|
|
936
|
-
|
|
368
|
+
Reserved variables are `_tag`, `name`, `message`, `messageTemplate`, `fingerprint`, `stack`, and
|
|
369
|
+
`cause`.
|
|
937
370
|
|
|
938
|
-
|
|
939
|
-
import { findUsersIn } from 'imaginary-database'
|
|
940
|
-
// ^ assume findUsersIn has the following signature:
|
|
941
|
-
// findUsersIn(country: string): ResultAsync<Array<User>, Error>
|
|
371
|
+
For lightweight tagged domain unions that do not need to extend `Error`, use `TaggedEnum`:
|
|
942
372
|
|
|
943
|
-
|
|
373
|
+
```ts
|
|
374
|
+
import type { TaggedEnum } from 'resultar'
|
|
944
375
|
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
376
|
+
type PaymentError = TaggedEnum<{
|
|
377
|
+
CardDeclined: { readonly code: string }
|
|
378
|
+
InsufficientFunds: { readonly balance: number }
|
|
379
|
+
}>
|
|
948
380
|
|
|
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
|
-
})
|
|
381
|
+
const error: PaymentError = { _tag: 'CardDeclined', code: 'card_declined' }
|
|
958
382
|
```
|
|
959
383
|
|
|
960
|
-
|
|
384
|
+
## Tagged Errors With Result
|
|
961
385
|
|
|
962
|
-
|
|
386
|
+
Tagged errors are most useful as the `E` side of `StrictResult<T, E>` and
|
|
387
|
+
`StrictResultAsync<T, E>`.
|
|
963
388
|
|
|
964
|
-
|
|
389
|
+
```ts
|
|
390
|
+
import type { StrictResult } from 'resultar'
|
|
965
391
|
|
|
966
|
-
|
|
392
|
+
import { createTaggedError, ok } from 'resultar'
|
|
967
393
|
|
|
968
|
-
|
|
394
|
+
class InvalidEmailError extends createTaggedError({
|
|
395
|
+
name: 'InvalidEmailError',
|
|
396
|
+
message: 'Invalid email $email',
|
|
397
|
+
}) {}
|
|
969
398
|
|
|
970
|
-
|
|
399
|
+
class UserAlreadyExistsError extends createTaggedError({
|
|
400
|
+
name: 'UserAlreadyExistsError',
|
|
401
|
+
message: 'User $email already exists',
|
|
402
|
+
}) {}
|
|
971
403
|
|
|
972
|
-
|
|
404
|
+
class DatabaseError extends createTaggedError({
|
|
405
|
+
name: 'DatabaseError',
|
|
406
|
+
message: 'Database operation $operation failed',
|
|
407
|
+
}) {}
|
|
973
408
|
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
callback: (error: E) => F | Promise<F>
|
|
978
|
-
): ResultAsync<T, F> { ... }
|
|
409
|
+
type User = {
|
|
410
|
+
readonly email: string
|
|
411
|
+
readonly id: string
|
|
979
412
|
}
|
|
980
|
-
```
|
|
981
413
|
|
|
982
|
-
|
|
414
|
+
type CreateUserError = InvalidEmailError | UserAlreadyExistsError | DatabaseError
|
|
983
415
|
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
// ^ assume findUsersIn has the following signature:
|
|
987
|
-
// findUsersIn(country: string): ResultAsync<Array<User>, Error>
|
|
416
|
+
const validateEmail = (email: string): StrictResult<string, InvalidEmailError> =>
|
|
417
|
+
email.includes('@') ? ok(email) : InvalidEmailError.err({ email })
|
|
988
418
|
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
if(
|
|
993
|
-
return
|
|
419
|
+
const ensureUserDoesNotExist = (
|
|
420
|
+
email: string,
|
|
421
|
+
): StrictResult<string, UserAlreadyExistsError | DatabaseError> => {
|
|
422
|
+
if (email === 'taken@example.com') {
|
|
423
|
+
return UserAlreadyExistsError.err({ email })
|
|
994
424
|
}
|
|
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
425
|
|
|
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> { ... }
|
|
426
|
+
return ok(email)
|
|
1029
427
|
}
|
|
1030
|
-
```
|
|
1031
428
|
|
|
1032
|
-
|
|
429
|
+
const insertUser = (email: string): StrictResult<User, DatabaseError> =>
|
|
430
|
+
ok({ email, id: 'usr_123' })
|
|
1033
431
|
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
432
|
+
const createUser = (email: string): StrictResult<User, CreateUserError> =>
|
|
433
|
+
validateEmail(email)
|
|
434
|
+
.andThen(ensureUserDoesNotExist)
|
|
435
|
+
.andThen(insertUser)
|
|
1037
436
|
```
|
|
1038
437
|
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
---
|
|
1042
|
-
|
|
1043
|
-
#### `ResultAsync.andThen` (method)
|
|
1044
|
-
|
|
1045
|
-
Same idea as `map` above. Except the applied function must return a `Result` or `ResultAsync`.
|
|
1046
|
-
|
|
1047
|
-
`ResultAsync.andThen` always returns a `ResultAsync` no matter the return type of the applied function.
|
|
1048
|
-
|
|
1049
|
-
This is useful for when you need to do a subsequent computation using the inner `T` value, but that computation might fail.
|
|
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:**
|
|
438
|
+
`InvalidEmailError.err({ email })` returns `Result<never, InvalidEmailError>`. Because `never` is assignable to every success type, it works naturally in functions returning `Result<string, InvalidEmailError>`.
|
|
1054
439
|
|
|
1055
|
-
|
|
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`)
|
|
440
|
+
Use `catchTag` when one tagged error can be recovered locally:
|
|
1059
441
|
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
442
|
+
```ts
|
|
443
|
+
const result = createUser('bad-email').catchTag('InvalidEmailError', (error) =>
|
|
444
|
+
ok({
|
|
445
|
+
email: error.email,
|
|
446
|
+
id: 'draft_user',
|
|
447
|
+
}),
|
|
448
|
+
)
|
|
1065
449
|
```
|
|
1066
450
|
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
```typescript
|
|
1070
|
-
|
|
1071
|
-
import { validateUser } from 'imaginary-validator'
|
|
1072
|
-
import { insertUser } from 'imaginary-database'
|
|
1073
|
-
import { sendNotification } from 'imaginary-service'
|
|
1074
|
-
|
|
1075
|
-
// ^ assume validateUser, insertUser and sendNotification have the following signatures:
|
|
1076
|
-
// validateUser(user: User): Result<User, Error>
|
|
1077
|
-
// insertUser(user): ResultAsync<User, Error>
|
|
1078
|
-
// sendNotification(user): ResultAsync<void, Error>
|
|
1079
|
-
|
|
1080
|
-
const resAsync = validateUser(user)
|
|
1081
|
-
.andThen(insertUser)
|
|
1082
|
-
.andThen(sendNotification)
|
|
451
|
+
Use `catchTags` when a small set of tagged errors share local recovery:
|
|
1083
452
|
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
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
|
-
}
|
|
453
|
+
```ts
|
|
454
|
+
const result = createUser('taken@example.com').catchTags({
|
|
455
|
+
InvalidEmailError: (error) => ok({ email: error.email, id: 'draft_user' }),
|
|
456
|
+
UserAlreadyExistsError: (error) => ok({ email: error.email, id: 'existing_user' }),
|
|
1093
457
|
})
|
|
1094
458
|
```
|
|
1095
459
|
|
|
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
|
-
```
|
|
460
|
+
`catchTags` is intentionally partial: any unhandled error tag stays in the `Err` side. A separate
|
|
461
|
+
`partialCatchTags` API would duplicate this behavior.
|
|
1134
462
|
|
|
1135
|
-
|
|
1136
|
-
```typescript
|
|
1137
|
-
import { err } from "resultar"
|
|
1138
|
-
import { equal } from 'node:assert'
|
|
463
|
+
At the boundary, use `matchTags` for direct success and tagged-error response mapping:
|
|
1139
464
|
|
|
1140
|
-
|
|
1141
|
-
const
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
class Result<T, E> {
|
|
1164
|
-
log(fn: (t?: T, e?: E) => void): this {
|
|
465
|
+
```ts
|
|
466
|
+
const result = createUser('taken@example.com')
|
|
467
|
+
|
|
468
|
+
const response = result.matchTags(
|
|
469
|
+
(user) => ({
|
|
470
|
+
body: user,
|
|
471
|
+
statusCode: 201,
|
|
472
|
+
}),
|
|
473
|
+
{
|
|
474
|
+
InvalidEmailError: (err) => ({
|
|
475
|
+
body: { code: err._tag, message: err.message },
|
|
476
|
+
statusCode: 400,
|
|
477
|
+
}),
|
|
478
|
+
UserAlreadyExistsError: (err) => ({
|
|
479
|
+
body: { code: err._tag, message: err.message },
|
|
480
|
+
statusCode: 409,
|
|
481
|
+
}),
|
|
482
|
+
DatabaseError: (err) => ({
|
|
483
|
+
body: { code: err._tag, message: err.message },
|
|
484
|
+
statusCode: 500,
|
|
485
|
+
}),
|
|
486
|
+
},
|
|
487
|
+
)
|
|
1165
488
|
```
|
|
1166
489
|
|
|
1167
|
-
|
|
1168
|
-
|
|
490
|
+
If a handler is missing, TypeScript reports it. For partial boundary matching, use
|
|
491
|
+
`matchTagsPartial(okHandler, handlers, fallback)`:
|
|
1169
492
|
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
})
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
})
|
|
493
|
+
```ts
|
|
494
|
+
const response = result.matchTagsPartial(
|
|
495
|
+
(user) => ({
|
|
496
|
+
body: user,
|
|
497
|
+
statusCode: 201,
|
|
498
|
+
}),
|
|
499
|
+
{
|
|
500
|
+
InvalidEmailError: (err) => ({
|
|
501
|
+
body: { code: err._tag, message: err.message },
|
|
502
|
+
statusCode: 400,
|
|
503
|
+
}),
|
|
504
|
+
},
|
|
505
|
+
(err) => ({
|
|
506
|
+
body: { code: 'UnhandledError', message: err.message },
|
|
507
|
+
statusCode: 500,
|
|
508
|
+
}),
|
|
509
|
+
)
|
|
1186
510
|
```
|
|
1187
|
-
[⬆️ Back to top](#toc)
|
|
1188
511
|
|
|
1189
|
-
|
|
512
|
+
Use `catchTags` for pipeline recovery and `matchTagsPartial` for boundary mapping.
|
|
1190
513
|
|
|
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
|
-
```
|
|
514
|
+
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
515
|
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
const reader = ok(file) // content file is line 01
|
|
516
|
+
```ts
|
|
517
|
+
declare const error: InvalidEmailError | DatabaseError | Error
|
|
1203
518
|
|
|
1204
|
-
const
|
|
1205
|
-
|
|
519
|
+
const message = matchError(error, {
|
|
520
|
+
DatabaseError: (err) => err.message,
|
|
521
|
+
Error: (err) => err.message,
|
|
522
|
+
InvalidEmailError: (err) => err.message,
|
|
1206
523
|
})
|
|
1207
|
-
|
|
1208
|
-
if (resul.isOk()) {
|
|
1209
|
-
console.log(result.value) // print line 01
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
524
|
```
|
|
1213
|
-
[⬆️ Back to top](#toc)
|
|
1214
|
-
|
|
1215
|
-
---
|
|
1216
525
|
|
|
1217
|
-
|
|
526
|
+
## Partial Matching
|
|
1218
527
|
|
|
1219
|
-
|
|
528
|
+
Use `matchErrorPartial` when only some tagged errors need special handling.
|
|
1220
529
|
|
|
1221
|
-
|
|
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
|
-
```
|
|
1230
|
-
|
|
1231
|
-
[⬆️ Back to top](#toc)
|
|
1232
|
-
|
|
1233
|
-
---
|
|
530
|
+
```ts
|
|
531
|
+
import { matchErrorPartial } from 'resultar'
|
|
1234
532
|
|
|
1235
|
-
|
|
533
|
+
declare const error: InvalidEmailError | UserAlreadyExistsError | DatabaseError
|
|
1236
534
|
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
535
|
+
const message = matchErrorPartial(
|
|
536
|
+
error,
|
|
537
|
+
{
|
|
538
|
+
InvalidEmailError: (err) => `Invalid input: ${err.email}`,
|
|
539
|
+
},
|
|
540
|
+
(err) => err.message,
|
|
541
|
+
)
|
|
1244
542
|
```
|
|
1245
543
|
|
|
1246
|
-
|
|
1247
|
-
See [`Result.tap` (method)](#resulttap-method)
|
|
1248
|
-
|
|
544
|
+
## Causes
|
|
1249
545
|
|
|
1250
|
-
|
|
546
|
+
Tagged errors support native `cause` and cause-chain lookup.
|
|
1251
547
|
|
|
1252
|
-
|
|
548
|
+
```ts
|
|
549
|
+
import { findCause } from 'resultar'
|
|
1253
550
|
|
|
1254
|
-
|
|
551
|
+
const cause = new Error('connection refused')
|
|
552
|
+
const error = new DatabaseError({
|
|
553
|
+
cause,
|
|
554
|
+
operation: 'insert-user',
|
|
555
|
+
})
|
|
1255
556
|
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
```typescript
|
|
1260
|
-
class ResultAsync<T, E> {
|
|
1261
|
-
tapError(f: (e: E) => void | Promise<void>): ResultAsync<T, E> {...}
|
|
1262
|
-
}
|
|
557
|
+
error.cause // cause
|
|
558
|
+
findCause(error, Error) // cause
|
|
559
|
+
error.findCause(Error) // cause
|
|
1263
560
|
```
|
|
1264
561
|
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
562
|
+
Cause lookup is cycle-safe: circular cause chains stop and return `undefined` when no matching cause
|
|
563
|
+
exists.
|
|
1268
564
|
|
|
1269
|
-
|
|
565
|
+
## Safe Try
|
|
1270
566
|
|
|
567
|
+
`safeTry` lets you unwrap successful results inside a generator and return early on the first error.
|
|
1271
568
|
|
|
1272
|
-
|
|
569
|
+
The shortest form is `yield* result`. `yield* result.safeUnwrap()` is still supported, but usually not necessary.
|
|
1273
570
|
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
**Signature:**
|
|
1277
|
-
```typescript
|
|
1278
|
-
class ResultAsync<T, E> {
|
|
1279
|
-
finally(f: (value: T, error: E) => void): DisposableResultAsync<T, E> {...}
|
|
1280
|
-
}
|
|
1281
|
-
```
|
|
1282
|
-
|
|
1283
|
-
**Example:**
|
|
1284
|
-
```typescript
|
|
1285
|
-
const fileHandle = await fs.open('foo.txt', 'w')
|
|
1286
|
-
|
|
1287
|
-
const result
|
|
1288
|
-
= await fromPromise(fileHandle.write('A new line of text'), String)
|
|
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
|
-
```
|
|
571
|
+
```ts
|
|
572
|
+
import type { Result, ResultAsync } from 'resultar'
|
|
1299
573
|
|
|
1300
|
-
|
|
574
|
+
import { ok, safeTry } from 'resultar'
|
|
1301
575
|
|
|
1302
|
-
|
|
576
|
+
declare const readConfig: () => Result<{ port: string }, Error>
|
|
577
|
+
declare const parsePort: (value: string) => Result<number, Error>
|
|
578
|
+
declare const persistPort: (port: number) => ResultAsync<void, Error>
|
|
1303
579
|
|
|
1304
|
-
|
|
580
|
+
const loadPort = (): Result<number, Error> =>
|
|
581
|
+
safeTry(function* () {
|
|
582
|
+
const config = yield* readConfig()
|
|
583
|
+
const port = yield* parsePort(config.port)
|
|
1305
584
|
|
|
1306
|
-
|
|
585
|
+
return ok(port)
|
|
586
|
+
})
|
|
1307
587
|
|
|
1308
|
-
|
|
588
|
+
const savePort = (): ResultAsync<number, Error> =>
|
|
589
|
+
safeTry(async function* () {
|
|
590
|
+
const config = yield* readConfig()
|
|
591
|
+
const port = yield* parsePort(config.port)
|
|
1309
592
|
|
|
1310
|
-
|
|
593
|
+
yield* persistPort(port)
|
|
1311
594
|
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
match<A>(
|
|
1315
|
-
okCallback: (value: T) => A,
|
|
1316
|
-
errorCallback: (error: E) => A
|
|
1317
|
-
): Promise<A> => { ... }
|
|
1318
|
-
}
|
|
595
|
+
return ok(port)
|
|
596
|
+
})
|
|
1319
597
|
```
|
|
1320
598
|
|
|
1321
|
-
|
|
599
|
+
## Side Effects
|
|
1322
600
|
|
|
1323
|
-
|
|
601
|
+
Use `tap`, `tapError`, `log`, and `finally` for best-effort observation and cleanup. They are not
|
|
602
|
+
transform methods: use `map`, `mapErr`, `andThen`, or `orElse` when the value or error should change.
|
|
1324
603
|
|
|
1325
|
-
|
|
1326
|
-
import { insertUser } from 'imaginary-database'
|
|
604
|
+
`tap(fn)` runs only for `Ok` and returns the original result unchanged.
|
|
1327
605
|
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
// insertUser(user): ResultAsync<User, Error>
|
|
606
|
+
```ts
|
|
607
|
+
import { ok } from 'resultar'
|
|
1331
608
|
|
|
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
|
-
)
|
|
609
|
+
const result = ok({ id: 'usr_123' }).tap((user) => {
|
|
610
|
+
console.info('user loaded', user.id)
|
|
611
|
+
})
|
|
1339
612
|
|
|
1340
|
-
//
|
|
613
|
+
// still Result<{ id: string }, never>
|
|
614
|
+
result
|
|
1341
615
|
```
|
|
1342
616
|
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
---
|
|
1346
|
-
|
|
1347
|
-
#### `ResultAsync.combine` (static class method)
|
|
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.
|
|
617
|
+
`tapError(fn)` runs only for `Err` and returns the original result unchanged.
|
|
1354
618
|
|
|
1355
|
-
|
|
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
|
-
```
|
|
619
|
+
```ts
|
|
620
|
+
import { err } from 'resultar'
|
|
1372
621
|
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
[okAsync(1), okAsync(2)]
|
|
622
|
+
const result = err(new Error('database unavailable')).tapError((error) => {
|
|
623
|
+
console.error('load failed', error)
|
|
624
|
+
})
|
|
1377
625
|
|
|
1378
|
-
|
|
1379
|
-
|
|
626
|
+
// still Result<never, Error>
|
|
627
|
+
result
|
|
1380
628
|
```
|
|
1381
629
|
|
|
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
|
|
630
|
+
`log(fn)` runs for both states. For `Ok`, the callback receives `(value, undefined)`. For `Err`, it
|
|
631
|
+
receives `(undefined, error)`.
|
|
1386
632
|
|
|
1387
|
-
|
|
1388
|
-
|
|
633
|
+
```ts
|
|
634
|
+
const result = findUser('usr_123').log((user, error) => {
|
|
635
|
+
if (error) {
|
|
636
|
+
console.error('find user failed', error)
|
|
637
|
+
return
|
|
638
|
+
}
|
|
1389
639
|
|
|
1390
|
-
|
|
1391
|
-
|
|
640
|
+
console.info('find user succeeded', user)
|
|
641
|
+
})
|
|
1392
642
|
```
|
|
1393
|
-
[⬆️ Back to top](#toc)
|
|
1394
|
-
|
|
1395
|
-
---
|
|
1396
643
|
|
|
1397
|
-
|
|
644
|
+
For `ResultAsync`, these callbacks may be synchronous or asynchronous. The original async result is
|
|
645
|
+
still preserved, even if the callback throws or rejects.
|
|
1398
646
|
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
```typescript
|
|
1406
|
-
// homogeneous lists
|
|
1407
|
-
function combineWithAllErrors<T, E>(resultList: ResultAsync<T, E>[]): ResultAsync<T[], E[]>
|
|
1408
|
-
|
|
1409
|
-
// heterogeneous lists
|
|
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
|
|
647
|
+
```ts
|
|
648
|
+
const result = fetchUser('usr_123')
|
|
649
|
+
.tap((user) => metrics.increment('user.found', { id: user.id }))
|
|
650
|
+
.tapError((error) => metrics.increment('user.failed', { message: error.message }))
|
|
651
|
+
.log((user, error) => audit.write({ error, user }))
|
|
1414
652
|
```
|
|
1415
653
|
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
const resultList: ResultAsync<number, string>[] = [
|
|
1420
|
-
okAsync(123),
|
|
1421
|
-
errAsync('boooom!'),
|
|
1422
|
-
okAsync(456),
|
|
1423
|
-
errAsync('ahhhhh!'),
|
|
1424
|
-
]
|
|
654
|
+
`finally(fn)` runs a best-effort cleanup callback with both `(value, error)`. On `Result`, the
|
|
655
|
+
callback runs before the method returns. On `ResultAsync`, it runs when the inner promise settles to a
|
|
656
|
+
`Result`, and async cleanup is awaited before the original result is returned.
|
|
1425
657
|
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
658
|
+
```ts
|
|
659
|
+
const result = findUser('usr_123').finally((_user, _error) => {
|
|
660
|
+
span.end()
|
|
661
|
+
})
|
|
1429
662
|
```
|
|
1430
663
|
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
664
|
+
Callback errors thrown by `tap`, `tapError`, `log`, and `finally` are intentionally ignored so
|
|
665
|
+
observability or cleanup code does not replace the original result. For `ResultAsync`, rejected
|
|
666
|
+
callback promises are ignored too. Put fallible work in `andThen`, `orElse`, `tryCatch`, or
|
|
667
|
+
`tryCatchAsync` when those failures should become part of the result type.
|
|
1434
668
|
|
|
1435
|
-
|
|
669
|
+
## Coming From Other Styles
|
|
1436
670
|
|
|
1437
|
-
|
|
671
|
+
### From try/catch
|
|
1438
672
|
|
|
1439
|
-
|
|
673
|
+
Use `tryCatch` and `tryCatchAsync` only where uncontrolled code can throw. Convert that failure into
|
|
674
|
+
a typed error once, then return `Result` values from your own code.
|
|
1440
675
|
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
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
|
-
}
|
|
676
|
+
```ts
|
|
677
|
+
const parseConfig = (input: string): StrictResult<Config, ParseConfigError> =>
|
|
678
|
+
tryCatch(
|
|
679
|
+
() => JSON.parse(input) as Config,
|
|
680
|
+
(cause) => new ParseConfigError({ cause }),
|
|
681
|
+
)
|
|
1470
682
|
```
|
|
1471
683
|
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
684
|
+
### From neverthrow-style wrappers
|
|
685
|
+
|
|
686
|
+
Resultar keeps the wrapper model, but recommends tagged `Error` values for production error
|
|
687
|
+
channels. Use `.value` only after `isOk()` and `.error` only after `isErr()`, or prefer `match` /
|
|
688
|
+
`matchTags` at boundaries.
|
|
689
|
+
|
|
690
|
+
### From raw `T | Error`
|
|
691
|
+
|
|
692
|
+
Raw unions are concise, but they rely on a convention that successful values are never `Error`
|
|
693
|
+
instances. Resultar keeps success and failure structurally separate, so `Ok<Error>` is still a valid
|
|
694
|
+
success value when a domain needs it.
|
|
695
|
+
|
|
696
|
+
### From Effect
|
|
697
|
+
|
|
698
|
+
Resultar does not provide dependency injection, fibers, scopes, schedules, streams, or a runtime. It
|
|
699
|
+
borrows the useful discipline of typed expected errors and composable recovery while staying a small
|
|
700
|
+
library around `Result` and `ResultAsync`.
|
|
701
|
+
|
|
702
|
+
## API
|
|
703
|
+
|
|
704
|
+
### Result Helpers
|
|
705
|
+
|
|
706
|
+
- `ok(value)`
|
|
707
|
+
- `err(error)`
|
|
708
|
+
- `unit()`
|
|
709
|
+
- `tryCatch(fn, errorFn?)`
|
|
710
|
+
- `fromThrowable(fn, errorFn?)`
|
|
711
|
+
- `safeTry(generator)`
|
|
712
|
+
- `StrictResult<T, E extends Error>` type alias for Error-only failure channels
|
|
713
|
+
|
|
714
|
+
### Result Methods
|
|
715
|
+
|
|
716
|
+
- `isOk()`
|
|
717
|
+
- `isErr()`
|
|
718
|
+
- `map(fn)`
|
|
719
|
+
- `mapErr(fn)`
|
|
720
|
+
- `filterOrElse(predicate, onFalse)`
|
|
721
|
+
- `andThen(fn)`
|
|
722
|
+
- `asyncAndThen(fn)`
|
|
723
|
+
- `orElse(fn)`
|
|
724
|
+
- `catchTag(tag, fn)`
|
|
725
|
+
- `catchTags(handlers)`
|
|
726
|
+
- `tap(fn)` run a side effect only when `Ok`; preserves the original result
|
|
727
|
+
- `tapError(fn)` run a side effect only when `Err`; preserves the original result
|
|
728
|
+
- `log(fn)` run a side effect for both states as `(value, error)`; preserves the original result
|
|
729
|
+
- `finally(fn)` run best-effort cleanup with `(value, error)`; callback errors are ignored
|
|
730
|
+
- `match(okFn, errFn)`
|
|
731
|
+
- `matchTags(okFn, handlers)`
|
|
732
|
+
- `matchTagsPartial(okFn, handlers, fallback)`
|
|
733
|
+
- `pipe(fn, ...)`
|
|
734
|
+
- `asyncMap(fn)`
|
|
735
|
+
- `unwrapOr(defaultValue)`
|
|
736
|
+
- `unwrapOrThrow()`
|
|
737
|
+
- `_unsafeUnwrap()`
|
|
738
|
+
- `_unsafeUnwrapErr()`
|
|
739
|
+
- `safeUnwrap()`
|
|
740
|
+
|
|
741
|
+
### Result Static Methods
|
|
742
|
+
|
|
743
|
+
- `Result.ok(value)`
|
|
744
|
+
- `Result.err(error)`
|
|
745
|
+
- `Result.unit()`
|
|
746
|
+
- `Result.tryCatch(fn, errorFn?)`
|
|
747
|
+
- `Result.fromThrowable(fn, errorFn?)`
|
|
748
|
+
- `Result.combine(results)`
|
|
749
|
+
- `Result.combineWithAllErrors(results)`
|
|
750
|
+
|
|
751
|
+
### ResultAsync Helpers
|
|
752
|
+
|
|
753
|
+
- `okAsync(value)`
|
|
754
|
+
- `errAsync(error)`
|
|
755
|
+
- `unitAsync()`
|
|
756
|
+
- `fromPromise(promise, errorFn)`
|
|
757
|
+
- `fromSafePromise(promise)`
|
|
758
|
+
- `fromThrowableAsync(fn, errorFn?)`
|
|
759
|
+
- `tryCatchAsync(promiseOrFn, errorFn?)` catches rejections and synchronous throws from factories
|
|
760
|
+
- `safeTryAsync(generator)` deprecated; use `safeTry`
|
|
761
|
+
- `StrictResultAsync<T, E extends Error>` type alias for Error-only async failure channels
|
|
762
|
+
|
|
763
|
+
### ResultAsync Methods
|
|
764
|
+
|
|
765
|
+
- `map(fn)`
|
|
766
|
+
- `mapErr(fn)`
|
|
767
|
+
- `andThen(fn)`
|
|
768
|
+
- `orElse(fn)`
|
|
769
|
+
- `tap(fn)` run a sync or async side effect only when `Ok`; preserves the original result
|
|
770
|
+
- `tapError(fn)` run a sync or async side effect only when `Err`; preserves the original result
|
|
771
|
+
- `log(fn)` run a sync or async side effect for both states as `(value, error)`
|
|
772
|
+
- `finally(fn)` run best-effort cleanup with `(value, error)` after the inner promise settles to a
|
|
773
|
+
`Result`; callback errors and rejections are ignored
|
|
774
|
+
- `match(okFn, errFn)`
|
|
775
|
+
- `matchTags(okFn, handlers)`
|
|
776
|
+
- `matchTagsPartial(okFn, handlers, fallback)`
|
|
777
|
+
- `unwrapOr(defaultValue)`
|
|
778
|
+
- `unwrapOrThrow()`
|
|
779
|
+
- `safeUnwrap()`
|
|
780
|
+
|
|
781
|
+
### Tagged Error Helpers
|
|
782
|
+
|
|
783
|
+
- `createTaggedError(options)`
|
|
784
|
+
- `matchError(error, handlers)`
|
|
785
|
+
- `matchErrorPartial(error, handlers, fallback)`
|
|
786
|
+
- `isError(value)`
|
|
787
|
+
- `findCause(error, ErrorClass)`
|
|
788
|
+
- `TaggedEnum<Members>` type helper
|
|
789
|
+
|
|
790
|
+
## Testing
|
|
791
|
+
|
|
792
|
+
`_unsafeUnwrap()` and `_unsafeUnwrapErr()` are intended for tests.
|
|
1475
793
|
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
### Utilities
|
|
1479
|
-
|
|
1480
|
-
#### `fromThrowable`
|
|
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
|
-
```
|
|
794
|
+
```ts
|
|
795
|
+
import { ok } from 'resultar'
|
|
1584
796
|
|
|
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
|
-
}
|
|
797
|
+
expect(ok(42)._unsafeUnwrap()).toBe(42)
|
|
1611
798
|
```
|
|
1612
799
|
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
[⬆️ Back to top](#toc)
|
|
1616
|
-
|
|
1617
|
-
---
|
|
800
|
+
By default, thrown unwrap errors omit stack traces for cleaner test output. Pass `withStackTrace` if needed:
|
|
1618
801
|
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
`Result` instances have two unsafe methods, aptly called `_unsafeUnwrap` and `_unsafeUnwrapErr` which **should only be used in a test environment**.
|
|
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)
|
|
802
|
+
```ts
|
|
803
|
+
result._unsafeUnwrapErr({ withStackTrace: true })
|
|
1631
804
|
```
|
|
1632
805
|
|
|
1633
|
-
|
|
806
|
+
## Development
|
|
1634
807
|
|
|
1635
|
-
|
|
1636
|
-
import { ok } from 'resultar'
|
|
808
|
+
This repository uses Vite+.
|
|
1637
809
|
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
810
|
+
```sh
|
|
811
|
+
pnpm install
|
|
812
|
+
pnpm run fmt:check
|
|
813
|
+
pnpm run lint
|
|
814
|
+
pnpm test
|
|
815
|
+
pnpm run test:cov
|
|
816
|
+
pnpm run build
|
|
817
|
+
pnpm run check:full
|
|
1641
818
|
```
|
|
1642
819
|
|
|
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
|
-
```
|
|
820
|
+
Package output:
|
|
1652
821
|
|
|
1653
|
-
|
|
822
|
+
- `dist/index.js`
|
|
823
|
+
- `dist/index.js.map`
|
|
824
|
+
- `dist/index.d.ts`
|
|
1654
825
|
|
|
1655
|
-
|
|
826
|
+
## License
|
|
1656
827
|
|
|
1657
|
-
|
|
828
|
+
MIT
|