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 CHANGED
@@ -1,1657 +1,877 @@
1
1
  # Resultar
2
2
 
3
- ## About
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
- ```sh
85
- > npm install resultar
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
- [⬆️ 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
- ```
9
+ Resultar models fallible work explicitly:
162
10
 
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> { ... }
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
- #### `Result.isOk` (method)
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
- Returns `true` if the result is an `Ok` variant
23
+ ## Requirements
191
24
 
192
- **Signature:**
25
+ - ESM only
26
+ - Node.js 24+
27
+ - TypeScript 6-ready
193
28
 
194
- ```typescript
195
- isOk(): boolean { ... }
29
+ ```ts
30
+ import { ok, err, createTaggedError } from 'resultar'
196
31
  ```
197
32
 
198
- [⬆️ Back to top](#toc)
199
-
200
- ---
201
-
202
- #### `Result.isErr` (method)
33
+ CommonJS `require('resultar')` is not exported.
203
34
 
204
- Returns `true` if the result is an `Err` variant
35
+ ## Install
205
36
 
206
- **Signature**:
207
-
208
- ```typescript
209
- isErr(): boolean { ... }
37
+ ```sh
38
+ pnpm add resultar
210
39
  ```
211
40
 
212
- [⬆️ Back to top](#toc)
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
- **Example**:
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
- // this Result now has a Array<number> inside it
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
- [⬆️ Back to top](#toc)
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
- #### `Result.mapErr` (method)
58
+ ## Philosophy
255
59
 
256
- Maps a `Result<T, E>` to `Result<T, F>` by applying a function to a contained `Err` value, leaving an `Ok` value untouched.
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
- This function can be used to pass through a successful result while handling an error.
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
- **Signature:**
67
+ The core model stays intentionally simple:
261
68
 
262
- ```typescript
263
- class Result<T, E> {
264
- mapErr<F>(callback: (error: E) => F): Result<T, F> { ... }
265
- }
69
+ ```ts
70
+ Result<T, E>
71
+ ResultAsync<T, E>
266
72
  ```
267
73
 
268
- **Example**:
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
- ```typescript
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
- const rawHeaders = 'nonsensical gibberish and badly formatted stuff'
80
+ For backend and application code, prefer this stack:
276
81
 
277
- const parseResult = parseHeaders(rawHeaders)
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
- parseResult.mapErr(parseError => {
280
- res.status(400).json({
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
- Unwrap the `Ok` value, or return the default if there is an `Err`
91
+ import { createTaggedError, ok } from 'resultar'
295
92
 
296
- **Signature:**
93
+ class UserAlreadyExistsError extends createTaggedError({
94
+ name: 'UserAlreadyExistsError',
95
+ message: 'User $email already exists',
96
+ }) {}
297
97
 
298
- ```typescript
299
- class Result<T, E> {
300
- unwrapOr<T>(value: T): T { ... }
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
- **Example**:
305
-
306
- ```typescript
307
- const myResult = err('Oh noooo')
104
+ Avoid using strings or loose objects for production-facing expected failures:
308
105
 
309
- const multiply = (value: number): number => value * 2
310
-
311
- const unwrapped: number = myResult.map(multiply).unwrapOr(10)
106
+ ```ts
107
+ // Local-only style. Avoid at service boundaries.
108
+ type ParseError = 'InvalidPort'
312
109
  ```
313
110
 
314
- [⬆️ Back to top](#toc)
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
- 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.
113
+ Resultar ships a type-aware check for discarded `Result` and `ResultAsync` values:
323
114
 
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> { ... }
115
+ ```json
116
+ {
117
+ "scripts": {
118
+ "lint:resultar": "resultar-no-discard --project tsconfig.json"
119
+ }
338
120
  }
339
121
  ```
340
122
 
341
- **Example 1: Chaining Results**
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
- ok(2)
349
- .andThen(sq)
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
- [⬆️ Back to top](#toc)
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
- ```typescript
388
- class Result<T, E> {
389
- asyncAndThen<U, F>(
390
- callback: (value: T) => ResultAsync<U, F>
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
- [⬆️ Back to top](#toc)
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
- ```typescript
406
- class Result<T, E> {
407
- orElse<A>(
408
- callback: (error: E) => Result<T, A>
409
- ): Result<T, A> { ... }
139
+ ```json
140
+ {
141
+ "scripts": {
142
+ "lint": "oxlint && resultar-no-discard --project tsconfig.json"
143
+ }
410
144
  }
411
145
  ```
412
146
 
413
- **Example:**
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
- [⬆️ Back to top](#toc)
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
- #### `Result.match` (method)
154
+ Use `mapErr` when the error value changes. Use `orElse` when an error should recover into another
155
+ `Result`.
441
156
 
442
- Given 2 functions (one for the `Ok` variant and one for the `Err` variant) execute the function that matches the `Result` variant.
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
- Match callbacks do not necessitate to return a `Result`, however you can return a `Result` if you want to.
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
- **Signature:**
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
- ```typescript
449
- class Result<T, E> {
450
- match<A>(
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
- `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
- ```
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
- const answer = computationThatMightFail().match(
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
- [⬆️ Back to top](#toc)
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
- #### `Result.asyncMap` (method)
510
-
511
- Similar to `map` except for two things:
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
- ```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("{");
191
+ const message = parsePort('3000').match(
192
+ (port) => `Listening on ${port}`,
193
+ (error) => error.message,
194
+ )
573
195
  ```
574
196
 
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:
197
+ ## Strict Results
594
198
 
595
- ```typescript
596
- // homogeneous lists
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
- // 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
- ```
202
+ ```ts
203
+ import type { StrictResult } from 'resultar'
615
204
 
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
205
+ import { createTaggedError, ok } from 'resultar'
620
206
 
621
- const resultTuple: [Result<string, never>, Result<string, never>] =
622
- tuple(ok('a'), ok('b'))
207
+ class InvalidEmailError extends createTaggedError({
208
+ name: 'InvalidEmailError',
209
+ message: 'Invalid email $email',
210
+ }) {}
623
211
 
624
- const combinedTuple: Result<[string, string], unknown> =
625
- Result.combine(resultTuple)
212
+ const validateEmail = (email: string): StrictResult<string, InvalidEmailError> =>
213
+ email.includes('@') ? ok(email) : InvalidEmailError.err({ email })
626
214
  ```
627
215
 
628
- [⬆️ Back to top](#toc)
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
- #### `Result.combineWithAllErrors` (static class method)
222
+ For expected application and domain failures, prefer `createTaggedError` classes and return them
223
+ through `StrictResult` or `StrictResultAsync`.
633
224
 
634
- > Although Result is not an actual JS class, the way that `combineWithAllErrors` has been implemented requires that you call `combineWithAllErrors` as though it were a static method on `Result`. See examples below.
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
225
+ ```ts
226
+ class UserAlreadyExistsError extends createTaggedError({
227
+ name: 'UserAlreadyExistsError',
228
+ message: 'User $email already exists',
229
+ }) {}
651
230
  ```
652
231
 
653
- Example usage:
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
- ```typescript
656
- const resultList: Result<number, string>[] = [
657
- ok(123),
658
- err('boooom!'),
659
- ok(456),
660
- err('ahhhhh!'),
661
- ]
237
+ ## Result
662
238
 
663
- const result = Result.combineWithAllErrors(resultList)
239
+ `Result<T, E>` is either `Ok<T>` or `Err<E>`.
664
240
 
665
- // result is Err(['boooom!', 'ahhhhh!'])
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
- [⬆️ Back to top](#toc)
244
+ import { createTaggedError, ok } from 'resultar'
675
245
 
676
- #### `Result.tryCatch()` (static class method)
246
+ class UserNotFoundError extends createTaggedError({
247
+ name: 'UserNotFoundError',
248
+ message: 'User $id not found',
249
+ }) {}
677
250
 
678
- Provides a convenient way to handle errors in a procedural style, similar to Go's error handling pattern. It wraps a synchronous function call in a try-catch block and returns a Result that can be destructured to access the value or error.
251
+ const findUser = (id: string): StrictResult<{ id: string }, UserNotFoundError> =>
252
+ id === 'usr_123' ? ok({ id }) : UserNotFoundError.err({ id })
679
253
 
680
- **Signature:**
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
- **Example:**
686
- ```typescript
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
- console.error(result.error); // Division error: Cannot divide by zero
261
+ result.error.message
707
262
  }
708
263
  ```
709
264
 
265
+ ### Chaining
710
266
 
711
- [⬆️ Back to top](#toc)
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
- [⬆️ 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`
762
-
763
- myResult.isOk() // false
764
- myResult.isErr() // true
765
- ```
269
+ ```ts
270
+ import type { StrictResult } from 'resultar'
766
271
 
767
- [⬆️ Back to top](#toc)
272
+ import { createTaggedError, ok } from 'resultar'
768
273
 
769
- ---
274
+ type User = {
275
+ readonly email: string
276
+ }
770
277
 
771
- #### `unitAsync`
278
+ class InvalidEmailError extends createTaggedError({
279
+ name: 'InvalidEmailError',
280
+ message: 'Invalid email $email',
281
+ }) {}
772
282
 
773
- Constructs a `ResultAsync` with a `value` of `undefined`.
283
+ class InvalidDomainError extends createTaggedError({
284
+ name: 'InvalidDomainError',
285
+ message: 'Invalid email domain $domain',
286
+ }) {}
774
287
 
775
- **Signature:**
288
+ const validateEmail = (email: string): StrictResult<string, InvalidEmailError> =>
289
+ email.includes('@') ? ok(email) : InvalidEmailError.err({ email })
776
290
 
777
- ```typescript
778
- unit<E = never>(error: E): ResultAsync<undefined, E>
291
+ const createUser = (email: string): StrictResult<User, InvalidEmailError> =>
292
+ validateEmail(email).map((validEmail) => ({ email: validEmail }))
779
293
  ```
780
294
 
781
- **Example:**
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
- const myResult = await myResultAsync // instance of `Err`
789
-
790
- myResult.isOk() // false
791
- myResult.value() // undefined
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
- [⬆️ Back to top](#toc)
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
- #### `ResultAsync.fromThrowable` (static class method)
799
-
800
- Similar to [Result.fromThrowable](#resultfromthrowable-static-class-method), but for functions that return a `Promise`.
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 insertUser = ResultAsync.fromThrowable(insertIntoDb, () => new Error('Database error'))
810
- // `res` has a type of (user: User) => ResultAsync<User, Error>
318
+ const result = createUser('person@example.com').pipe(
319
+ audit,
320
+ (userResult) => userResult.map((user) => user.email),
321
+ )
811
322
  ```
812
323
 
813
- Note that this can be safer than using [ResultAsync.fromPromise](#resultasyncfrompromise-static-class-method) with
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
- ```typescript
818
- import { readFile } from 'node:fs/promises'
819
- import { fromThrowableAsync } from 'resultar'
326
+ Use `match` at boundaries.
820
327
 
821
- const safeFileReader = fromThrowableAsync(
822
- async (file: string) => readFile(file),
823
- e => new Error('Oops: '.concat(String(e))),
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
- [⬆️ Back to top](#toc)
335
+ ## ResultAsync
833
336
 
834
- ---
337
+ `ResultAsync<T, E>` wraps `Promise<Result<T, E>>` and keeps the same composition style.
835
338
 
836
- #### `ResultAsync.fromPromise` (static class method)
339
+ ```ts
340
+ import type { StrictResultAsync } from 'resultar'
837
341
 
838
- Transforms a `PromiseLike<T>` (that may throw) into a `ResultAsync<T, E>`.
342
+ import { createTaggedError, fromPromise } from 'resultar'
839
343
 
840
- The second argument handles the rejection case of the promise and maps the error from `unknown` into some type `E`.
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
- **Signature:**
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
- ```typescript
846
- // fromPromise is a static class method
847
- // also available as a standalone function
848
- // import { fromPromise } from 'resultar'
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
- If you are working with `PromiseLike` objects that you **know for a fact** will not throw, then use `fromSafePromise` in order to avoid having to pass a redundant `errorHandler` argument.
365
+ ## Tagged Errors
856
366
 
857
- **Example**:
367
+ Tagged errors are real `Error` subclasses with:
858
368
 
859
- ```typescript
860
- import { ResultAsync } from 'resultar'
861
- import { insertIntoDb } from 'imaginary-database'
862
- // insertIntoDb(user: User): Promise<User>
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
- const res = ResultAsync.fromPromise(insertIntoDb(myUser), () => new Error('Database error'))
865
- // `res` has a type of ResultAsync<User, Error>
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
- Same as `ResultAsync.fromPromise` except that it does not handle the rejection of the promise. **Ensure you know what you're doing, otherwise a thrown exception within this promise will cause ResultAsync to reject, instead of resolve to a Result.**
382
+ class UserNotFoundError extends createTaggedError({
383
+ name: 'UserNotFoundError',
384
+ message: 'User $id not found in $source',
385
+ }) {}
875
386
 
876
- **Signature:**
387
+ const error = new UserNotFoundError({
388
+ id: 'usr_123',
389
+ source: 'database',
390
+ })
877
391
 
878
- ```typescript
879
- // fromPromise is a static class method
880
- // also available as a standalone function
881
- // import { fromPromise } from 'resultar'
882
- ResultAsync.fromSafePromise<T, E>(
883
- promise: PromiseLike<T>
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
- **Example**:
888
-
889
- ```typescript
890
- import { RouteError } from 'routes/error'
400
+ Missing template props are TypeScript errors:
891
401
 
892
- // simulate slow routes in an http server that works in a Result / ResultAsync context
893
- // Adopted from https://github.com/parlez-vous/server/blob/2496bacf55a2acbebc30631b5562f34272794d76/src/routes/common/signup.ts
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
- export const signupHandler = route<User>((req, sessionManager) =>
904
- decode(userSignupDecoder, req.body, 'Invalid request body').map((parsed) => {
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
- [⬆️ Back to top](#toc)
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
- **Example**:
417
+ Reserved variables are `_tag`, `name`, `message`, `messageTemplate`, `fingerprint`, `stack`, and
418
+ `cause`.
937
419
 
938
- ```typescript
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
- const usersInCanada = findUsersIn("Canada")
422
+ ```ts
423
+ import type { TaggedEnum } from 'resultar'
944
424
 
945
- // Let's assume we only need their names
946
- const namesInCanada = usersInCanada.map((users: Array<User>) => users.map(user => user.name))
947
- // namesInCanada is of type ResultAsync<Array<string>, Error>
425
+ type PaymentError = TaggedEnum<{
426
+ CardDeclined: { readonly code: string }
427
+ InsufficientFunds: { readonly balance: number }
428
+ }>
948
429
 
949
- // We can extract the Result using .then() or await
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
- [⬆️ Back to top](#toc)
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
- #### `ResultAsync.mapErr` (method)
438
+ ```ts
439
+ import type { StrictResult } from 'resultar'
965
440
 
966
- Maps a `ResultAsync<T, E>` to `ResultAsync<T, F>` by applying a function to a contained `Err` value, leaving an `Ok` value untouched.
441
+ import { createTaggedError, ok } from 'resultar'
967
442
 
968
- The applied function can be synchronous or asynchronous (returning a `Promise<F>`) with no impact to the return type.
443
+ class InvalidEmailError extends createTaggedError({
444
+ name: 'InvalidEmailError',
445
+ message: 'Invalid email $email',
446
+ }) {}
969
447
 
970
- This function can be used to pass through a successful result while handling an error.
448
+ class UserAlreadyExistsError extends createTaggedError({
449
+ name: 'UserAlreadyExistsError',
450
+ message: 'User $email already exists',
451
+ }) {}
971
452
 
972
- **Signature:**
453
+ class DatabaseError extends createTaggedError({
454
+ name: 'DatabaseError',
455
+ message: 'Database operation $operation failed',
456
+ }) {}
973
457
 
974
- ```typescript
975
- class ResultAsync<T, E> {
976
- mapErr<F>(
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
- **Example**:
463
+ type CreateUserError = InvalidEmailError | UserAlreadyExistsError | DatabaseError
983
464
 
984
- ```typescript
985
- import { findUsersIn } from 'imaginary-database'
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
- // Let's say we need to low-level errors from findUsersIn to be more readable
990
- const usersInCanada = findUsersIn("Canada").mapErr((error: Error) => {
991
- // The only error we want to pass to the user is "Unknown country"
992
- if(error.message === "Unknown country"){
993
- return error.message
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
- [⬆️ Back to top](#toc)
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
- **Example**:
478
+ const insertUser = (email: string): StrictResult<User, DatabaseError> =>
479
+ ok({ email, id: 'usr_123' })
1033
480
 
1034
- ```typescript
1035
- const unwrapped: number = await errAsync(0).unwrapOr(10)
1036
- // unwrapped = 10
481
+ const createUser = (email: string): StrictResult<User, CreateUserError> =>
482
+ validateEmail(email)
483
+ .andThen(ensureUserDoesNotExist)
484
+ .andThen(insertUser)
1037
485
  ```
1038
486
 
1039
- [⬆️ Back to top](#toc)
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
- #### `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:**
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
- **Example**
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
- // ^ 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)
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
- [⬆️ Back to top](#toc)
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
- #### `Result.log` (method)
1157
-
1158
- **Executes a side effect function with the `Ok` or `Err` value and returns the original `Result`.
1159
- This method is useful for performing actions that do not modify the `Result` itself, such as logging or updating external state.**
1160
-
1161
- **Signature:**
1162
- ```typescript
1163
- class Result<T, E> {
1164
- log(fn: (t?: T, e?: E) => void): this {
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
- **Example:**
1168
- ```typescript
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
- reader
1173
- .log((value, error) => {
1174
- console.log("Logging file content:", error ? error : value)
1175
- })
1176
- .map(it => it.content)
1177
- .finally(_ => {
1178
- console.info('Closing file handle')
1179
- file.close()
1180
- })
1181
- .match((value) => {
1182
- console.log(value) // print line 01
1183
- }, (error) => {
1184
- console.error("Oops, something went wrong", error)
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
- #### `Result.finally` (method)
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
- **Example:**
1201
- ```typescript
1202
- const reader = ok(file) // content file is line 01
565
+ ```ts
566
+ declare const error: InvalidEmailError | DatabaseError | Error
1203
567
 
1204
- const result = reader.map(it => it.content).finally(_ => {
1205
- file.close()
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
- [⬆️ Back to top](#toc)
577
+ Use `matchErrorPartial` when only some tagged errors need special handling.
1232
578
 
1233
- ---
579
+ ```ts
580
+ import { matchErrorPartial } from 'resultar'
1234
581
 
1235
- #### `ResultAsync.tap` (method)
582
+ declare const error: InvalidEmailError | UserAlreadyExistsError | DatabaseError
1236
583
 
1237
- Executes a side effect function with the `Ok` value and returns the original `ResultAsync`.
1238
- This method is useful for performing actions that do not modify the `ResultAsyn` itself, such as logging or updating external state.
1239
- **Signature:**
1240
- ```typescript
1241
- class ResultAsync<T, E> {
1242
- tap(f: (t: T) => void | Promise<void>): ResultAsync<T, E> {...}
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
- **Example:**
1247
- See [`Result.tap` (method)](#resulttap-method)
593
+ ## Causes
1248
594
 
595
+ Tagged errors support native `cause` and cause-chain lookup.
1249
596
 
1250
- [⬆️ Back to top](#toc)
1251
-
1252
- ---
597
+ ```ts
598
+ import { findCause } from 'resultar'
1253
599
 
1254
- #### `ResultAsync.tapError` (method)
600
+ const cause = new Error('connection refused')
601
+ const error = new DatabaseError({
602
+ cause,
603
+ operation: 'insert-user',
604
+ })
1255
605
 
1256
- Executes a side effect function with the `Ok` value and returns the original `ResultAsync`.
1257
- This method is useful for performing actions that do not modify the `ResultAsyn` itself, such as logging or updating external state.
1258
- **Signature:**
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
- **Example:**
1266
- See [`Result.tapError` (method)](#resulttaperror-method)
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
- **Example:**
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
- 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
- ```
620
+ ```ts
621
+ import type { Result, ResultAsync } from 'resultar'
1299
622
 
1300
- [⬆️ Back to top](#toc)
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
- #### `ResultAsync.match` (method)
629
+ const loadPort = (): Result<number, Error> =>
630
+ safeTry(function* () {
631
+ const config = yield* readConfig()
632
+ const port = yield* parsePort(config.port)
1305
633
 
1306
- Given 2 functions (one for the `Ok` variant and one for the `Err` variant) execute the function that matches the `ResultAsync` variant.
634
+ return ok(port)
635
+ })
1307
636
 
1308
- The difference with `Result.match` is that it always returns a `Promise` because of the asynchronous nature of the `ResultAsync`.
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
- **Signature:**
642
+ yield* persistPort(port)
1311
643
 
1312
- ```typescript
1313
- class ResultAsync<T, E> {
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
- **Example:**
648
+ ## Side Effects
1322
649
 
1323
- ```typescript
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
- import { validateUser } from 'imaginary-validator'
1326
- import { insertUser } from 'imaginary-database'
653
+ `tap(fn)` runs only for `Ok` and returns the original result unchanged.
1327
654
 
1328
- // ^ assume validateUser and insertUser have the following signatures:
1329
- // validateUser(user: User): Result<User, Error>
1330
- // insertUser(user): ResultAsync<User, Error>
655
+ ```ts
656
+ import { ok } from 'resultar'
1331
657
 
1332
- // Handle both cases at the end of the chain using match
1333
- const resultMessage = await validateUser(user)
1334
- .andThen(insertUser)
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
- // resultMessage is a string
662
+ // still Result<{ id: string }, never>
663
+ result
1341
664
  ```
1342
665
 
1343
- [⬆️ Back to top](#toc)
1344
-
1345
- ---
666
+ `tapError(fn)` runs only for `Err` and returns the original result unchanged.
1346
667
 
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.
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
- Example:
1374
- ```typescript
1375
- const resultList: ResultAsync<number, never>[] =
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
- const combinedList: ResultAsync<number[], unknown> =
1379
- ResultAsync.combine(resultList)
675
+ // still Result<never, Error>
676
+ result
1380
677
  ```
1381
678
 
1382
- Example with tuples:
1383
- ```typescript
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
- const resultTuple: [ResultAsync<string, never>, ResultAsync<string, never>] =
1388
- tuple(okAsync('a'), okAsync('b'))
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
- const combinedTuple: ResultAsync<[string, string], unknown> =
1391
- ResultAsync.combine(resultTuple)
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
- Function signature:
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
- ```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
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
- Example usage:
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
- const result = ResultAsync.combineWithAllErrors(resultList)
706
+ ```ts
707
+ using result = findUser('usr_123').toDisposable((_user, _error) => {
708
+ span.end()
709
+ })
1427
710
 
1428
- // result is Err(['boooom!', 'ahhhhh!'])
711
+ await using asyncResult = fetchUser('usr_123').toAsyncDisposable(async (_user, _error) => {
712
+ await span.end()
713
+ })
1429
714
  ```
1430
715
 
1431
- #### `ResultAsync.safeUnwrap()`
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
- Allows for unwrapping a `Result` or returning an `Err` implicitly, thereby reducing boilerplate.
721
+ ## Coming From Other Styles
1434
722
 
1435
- [⬆️ Back to top](#toc)
723
+ ### From try/catch
1436
724
 
1437
- #### `ResultAsync.tryCatch()` (static class method)
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
- Similar to `Result.tryCatch`, but for handling asynchronous operations. It wraps a Promise in a try-catch block and returns a ResultAsync that can be awaited and destructured to access the value or error.
1440
-
1441
- **Signature:**
1442
- ```typescript
1443
- static tryCatch<T, E>(fn: Promise<T>, errorFn?: (e: unknown) => E): ResultAsync<T, E>
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
- The `tryCatch` methods provide a more familiar error handling pattern for developers coming from languages like Go, while still maintaining the type safety and composability of the Result type.
1473
-
1474
- [⬆️ Back to top](#toc)
1475
-
1476
- ---
1477
-
1478
- ### Utilities
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
- #### `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
- ```
843
+ ```ts
844
+ import { ok } from 'resultar'
1584
845
 
1585
- To use `safeTry`, the points are as follows.
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
- For more information, see https://github.com/supermacro/neverthrow/pull/448 and https://github.com/supermacro/neverthrow/issues/444
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
- `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)
851
+ ```ts
852
+ result._unsafeUnwrapErr({ withStackTrace: true })
1631
853
  ```
1632
854
 
1633
- However, do note that `Result` instances are comparable. So you don't necessarily need to unwrap them in order to assert expectations in your tests. So you could also do something like this:
1634
-
1635
- ```typescript
1636
- import { ok } from 'resultar'
855
+ ## Development
1637
856
 
1638
- // ...
857
+ This repository uses Vite+.
1639
858
 
1640
- expect(callSomeFunctionThatReturnsAResult("with", "some", "args")).toEqual(ok(someExpectation));
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
- By default, the thrown value does not contain a stack trace. This is because stack trace generation [makes error messages in Jest harder to understand](https://github.com/supermacro/neverthrow/pull/215). If you want stack traces to be generated, call `_unsafeUnwrap` and / or `_unsafeUnwrapErr` with a config object:
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
- ### License
875
+ ## License
1656
876
 
1657
- The resultar project is available as open source under the terms of the [MIT license](https://github.com/inaiat/resultar/blob/master/LICENSE).
877
+ MIT