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 CHANGED
@@ -1,1657 +1,828 @@
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
- ```
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
- Maps a `Result<T, E>` to `Result<U, E>` by applying a function to a contained `Ok` value, leaving an `Err` value untouched.
9
+ Resultar models fallible work explicitly:
219
10
 
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
- }
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
- **Example**:
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
- ```typescript
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
- // 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')
25
+ - ESM only
26
+ - Node.js 24+
27
+ - TypeScript 6-ready
241
28
 
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
29
+ ```ts
30
+ import { ok, err, createTaggedError } from 'resultar'
248
31
  ```
249
32
 
250
- [⬆️ Back to top](#toc)
33
+ CommonJS `require('resultar')` is not exported.
251
34
 
252
- ---
35
+ ## Install
253
36
 
254
- #### `Result.mapErr` (method)
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
- **Example**:
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
- [⬆️ Back to top](#toc)
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
- **Example**:
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
- ```typescript
307
- const myResult = err('Oh noooo')
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
- const multiply = (value: number): number => value * 2
54
+ The core model stays intentionally simple:
310
55
 
311
- const unwrapped: number = myResult.map(multiply).unwrapOr(10)
56
+ ```ts
57
+ Result<T, E>
58
+ ResultAsync<T, E>
312
59
  ```
313
60
 
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`.
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
- **Example 1: Chaining Results**
65
+ ## Recommended Production Style
342
66
 
343
- ```typescript
344
- import { err, ok } from 'resultar'
67
+ For backend and application code, prefer this stack:
345
68
 
346
- const sq = (n: number): Result<number, number> => ok(n ** 2)
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
- ok(2)
349
- .andThen(sq)
350
- .andThen(sq) // Ok(16)
75
+ ```ts
76
+ import type { StrictResult } from 'resultar'
351
77
 
352
- ok(2)
353
- .andThen(sq)
354
- .andThen(err) // Err(4)
78
+ import { createTaggedError, ok } from 'resultar'
355
79
 
356
- ok(2)
357
- .andThen(err)
358
- .andThen(sq) // Err(2)
80
+ class UserAlreadyExistsError extends createTaggedError({
81
+ name: 'UserAlreadyExistsError',
82
+ message: 'User $email already exists',
83
+ }) {}
359
84
 
360
- err(3)
361
- .andThen(sq)
362
- .andThen(sq) // Err(3)
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
- **Example 2: Flattening Nested Results**
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
- // notNested is a Ok(1234)
372
- const notNested = nested.andThen((innerResult) => innerResult)
93
+ ```ts
94
+ // Local-only style. Avoid at service boundaries.
95
+ type ParseError = 'InvalidPort'
373
96
  ```
374
97
 
375
- [⬆️ Back to top](#toc)
98
+ ## API Decision Guide
376
99
 
377
- ---
100
+ Use `map` when the `Ok` value changes and the transform cannot fail.
378
101
 
379
- #### `Result.asyncAndThen` (method)
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
- Same idea as [`andThen` above](#resultandthen-method), except you must return a new `ResultAsync`.
105
+ Use `mapErr` when the error value changes. Use `orElse` when an error should recover into another
106
+ `Result`.
382
107
 
383
- The returned value will be a `ResultAsync`.
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
- **Signature:**
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
- ```typescript
388
- class Result<T, E> {
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
- [⬆️ Back to top](#toc)
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
- #### `Result.orElse` (method)
124
+ ```ts
125
+ import type { StrictResult } from 'resultar'
400
126
 
401
- Takes an `Err` value and maps it to a `Result<T, SomeNewType>`. This is useful for error recovery.
127
+ import { createTaggedError, ok } from 'resultar'
402
128
 
403
- **Signature:**
129
+ class InvalidPortError extends createTaggedError({
130
+ name: 'InvalidPortError',
131
+ message: 'Invalid port $value',
132
+ }) {}
404
133
 
405
- ```typescript
406
- class Result<T, E> {
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
- ```typescript
416
- enum DatabaseError {
417
- PoolExhausted = 'PoolExhausted',
418
- NotFound = 'NotFound',
137
+ return Number.isInteger(port) && port > 0
138
+ ? ok(port)
139
+ : InvalidPortError.err({ value })
419
140
  }
420
141
 
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)
142
+ const message = parsePort('3000').match(
143
+ (port) => `Listening on ${port}`,
144
+ (error) => error.message,
433
145
  )
434
146
  ```
435
147
 
436
- [⬆️ Back to top](#toc)
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
- **Signature:**
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
- 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
- ```
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
- - the mapping function must return a `Promise`
514
- - asyncMap returns a `ResultAsync`
156
+ import { createTaggedError, ok } from 'resultar'
515
157
 
516
- You can then chain the result of `asyncMap` using the `ResultAsync` apis (like `map`, `mapErr`, `andThen`, etc.)
158
+ class InvalidEmailError extends createTaggedError({
159
+ name: 'InvalidEmailError',
160
+ message: 'Invalid email $email',
161
+ }) {}
517
162
 
518
- **Signature:**
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
- **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.
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
- **Example**:
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
- [⬆️ Back to top](#toc)
173
+ For expected application and domain failures, prefer `createTaggedError` classes and return them
174
+ through `StrictResult` or `StrictResultAsync`.
629
175
 
630
- ---
631
-
632
- #### `Result.combineWithAllErrors` (static class method)
633
-
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
176
+ ```ts
177
+ class UserAlreadyExistsError extends createTaggedError({
178
+ name: 'UserAlreadyExistsError',
179
+ message: 'User $email already exists',
180
+ }) {}
651
181
  ```
652
182
 
653
- Example usage:
654
-
655
- ```typescript
656
- const resultList: Result<number, string>[] = [
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
- // result is Err(['boooom!', 'ahhhhh!'])
666
- ```
667
-
668
- [⬆️ Back to top](#toc)
188
+ ## Result
669
189
 
670
- #### `Result.safeUnwrap()`
190
+ `Result<T, E>` is either `Ok<T>` or `Err<E>`.
671
191
 
672
- Allows for unwrapping a `Result` or returning an `Err` implicitly, thereby reducing boilerplate.
192
+ ```ts
193
+ import type { StrictResult } from 'resultar'
673
194
 
674
- [⬆️ Back to top](#toc)
195
+ import { createTaggedError, ok } from 'resultar'
675
196
 
676
- #### `Result.tryCatch()` (static class method)
197
+ class UserNotFoundError extends createTaggedError({
198
+ name: 'UserNotFoundError',
199
+ message: 'User $id not found',
200
+ }) {}
677
201
 
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.
202
+ const findUser = (id: string): StrictResult<{ id: string }, UserNotFoundError> =>
203
+ id === 'usr_123' ? ok({ id }) : UserNotFoundError.err({ id })
679
204
 
680
- **Signature:**
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
- **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
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
- console.error(result.error); // Division error: Cannot divide by zero
212
+ result.error.message
707
213
  }
708
214
  ```
709
215
 
216
+ ### Chaining
710
217
 
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
- ```
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
- myResult.isOk() // false
764
- myResult.isErr() // true
765
- ```
220
+ ```ts
221
+ import type { StrictResult } from 'resultar'
766
222
 
767
- [⬆️ Back to top](#toc)
223
+ import { createTaggedError, ok } from 'resultar'
768
224
 
769
- ---
225
+ type User = {
226
+ readonly email: string
227
+ }
770
228
 
771
- #### `unitAsync`
229
+ class InvalidEmailError extends createTaggedError({
230
+ name: 'InvalidEmailError',
231
+ message: 'Invalid email $email',
232
+ }) {}
772
233
 
773
- Constructs a `ResultAsync` with a `value` of `undefined`.
234
+ class InvalidDomainError extends createTaggedError({
235
+ name: 'InvalidDomainError',
236
+ message: 'Invalid email domain $domain',
237
+ }) {}
774
238
 
775
- **Signature:**
239
+ const validateEmail = (email: string): StrictResult<string, InvalidEmailError> =>
240
+ email.includes('@') ? ok(email) : InvalidEmailError.err({ email })
776
241
 
777
- ```typescript
778
- unit<E = never>(error: E): ResultAsync<undefined, E>
242
+ const createUser = (email: string): StrictResult<User, InvalidEmailError> =>
243
+ validateEmail(email).map((validEmail) => ({ email: validEmail }))
779
244
  ```
780
245
 
781
- **Example:**
782
-
783
- ```typescript
784
- import { errAsync } from 'resultar'
246
+ Use `filterOrElse` when a successful value must satisfy an extra predicate:
785
247
 
786
- const myResultAsync =unitAsync() // instance of `ResultAsync`
787
-
788
- const myResult = await myResultAsync // instance of `Err`
789
-
790
- myResult.isOk() // false
791
- myResult.value() // undefined
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
- [⬆️ Back to top](#toc)
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
- Similar to [Result.fromThrowable](#resultfromthrowable-static-class-method), but for functions that return a `Promise`.
262
+ Use `pipe` when you want to apply reusable Result combinators:
801
263
 
802
- **Example**:
803
-
804
- ```typescript
805
- import { ResultAsync } from 'neverthrow'
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 insertUser = ResultAsync.fromThrowable(insertIntoDb, () => new Error('Database error'))
810
- // `res` has a type of (user: User) => ResultAsync<User, Error>
269
+ const result = createUser('person@example.com').pipe(
270
+ audit,
271
+ (userResult) => userResult.map((user) => user.email),
272
+ )
811
273
  ```
812
274
 
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:
275
+ ### Matching
816
276
 
817
- ```typescript
818
- import { readFile } from 'node:fs/promises'
819
- import { fromThrowableAsync } from 'resultar'
277
+ Use `match` at boundaries.
820
278
 
821
- const safeFileReader = fromThrowableAsync(
822
- async (file: string) => readFile(file),
823
- e => new Error('Oops: '.concat(String(e))),
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
- [⬆️ Back to top](#toc)
286
+ ## ResultAsync
833
287
 
834
- ---
288
+ `ResultAsync<T, E>` wraps `Promise<Result<T, E>>` and keeps the same composition style.
835
289
 
836
- #### `ResultAsync.fromPromise` (static class method)
290
+ ```ts
291
+ import type { StrictResultAsync } from 'resultar'
837
292
 
838
- Transforms a `PromiseLike<T>` (that may throw) into a `ResultAsync<T, E>`.
293
+ import { createTaggedError, fromPromise } from 'resultar'
839
294
 
840
- The second argument handles the rejection case of the promise and maps the error from `unknown` into some type `E`.
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
- **Signature:**
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
- ```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> { ... }
310
+ const label = await fetchUser('usr_123').match(
311
+ (user) => `User ${user.id}`,
312
+ (error) => error.message,
313
+ )
853
314
  ```
854
315
 
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.
316
+ ## Tagged Errors
856
317
 
857
- **Example**:
318
+ Tagged errors are real `Error` subclasses with:
858
319
 
859
- ```typescript
860
- import { ResultAsync } from 'resultar'
861
- import { insertIntoDb } from 'imaginary-database'
862
- // insertIntoDb(user: User): Promise<User>
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
- 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)
330
+ ```ts
331
+ import { createTaggedError } from 'resultar'
873
332
 
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.**
333
+ class UserNotFoundError extends createTaggedError({
334
+ name: 'UserNotFoundError',
335
+ message: 'User $id not found in $source',
336
+ }) {}
875
337
 
876
- **Signature:**
338
+ const error = new UserNotFoundError({
339
+ id: 'usr_123',
340
+ source: 'database',
341
+ })
877
342
 
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> { ... }
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
- **Example**:
888
-
889
- ```typescript
890
- import { RouteError } from 'routes/error'
351
+ Missing template props are TypeScript errors:
891
352
 
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
- )
353
+ ```ts
354
+ new UserNotFoundError({ id: 'usr_123', source: 'database' })
902
355
 
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
- )
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
- [⬆️ 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
- }
363
+ ```ts
364
+ // @ts-expect-error name is reserved
365
+ createTaggedError({ name: 'BadError', message: 'Bad $name' })
934
366
  ```
935
367
 
936
- **Example**:
368
+ Reserved variables are `_tag`, `name`, `message`, `messageTemplate`, `fingerprint`, `stack`, and
369
+ `cause`.
937
370
 
938
- ```typescript
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
- const usersInCanada = findUsersIn("Canada")
373
+ ```ts
374
+ import type { TaggedEnum } from 'resultar'
944
375
 
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>
376
+ type PaymentError = TaggedEnum<{
377
+ CardDeclined: { readonly code: string }
378
+ InsufficientFunds: { readonly balance: number }
379
+ }>
948
380
 
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
- })
381
+ const error: PaymentError = { _tag: 'CardDeclined', code: 'card_declined' }
958
382
  ```
959
383
 
960
- [⬆️ Back to top](#toc)
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
- #### `ResultAsync.mapErr` (method)
389
+ ```ts
390
+ import type { StrictResult } from 'resultar'
965
391
 
966
- Maps a `ResultAsync<T, E>` to `ResultAsync<T, F>` by applying a function to a contained `Err` value, leaving an `Ok` value untouched.
392
+ import { createTaggedError, ok } from 'resultar'
967
393
 
968
- The applied function can be synchronous or asynchronous (returning a `Promise<F>`) with no impact to the return type.
394
+ class InvalidEmailError extends createTaggedError({
395
+ name: 'InvalidEmailError',
396
+ message: 'Invalid email $email',
397
+ }) {}
969
398
 
970
- This function can be used to pass through a successful result while handling an error.
399
+ class UserAlreadyExistsError extends createTaggedError({
400
+ name: 'UserAlreadyExistsError',
401
+ message: 'User $email already exists',
402
+ }) {}
971
403
 
972
- **Signature:**
404
+ class DatabaseError extends createTaggedError({
405
+ name: 'DatabaseError',
406
+ message: 'Database operation $operation failed',
407
+ }) {}
973
408
 
974
- ```typescript
975
- class ResultAsync<T, E> {
976
- mapErr<F>(
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
- **Example**:
414
+ type CreateUserError = InvalidEmailError | UserAlreadyExistsError | DatabaseError
983
415
 
984
- ```typescript
985
- import { findUsersIn } from 'imaginary-database'
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
- // 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
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
- [⬆️ 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> { ... }
426
+ return ok(email)
1029
427
  }
1030
- ```
1031
428
 
1032
- **Example**:
429
+ const insertUser = (email: string): StrictResult<User, DatabaseError> =>
430
+ ok({ email, id: 'usr_123' })
1033
431
 
1034
- ```typescript
1035
- const unwrapped: number = await errAsync(0).unwrapOr(10)
1036
- // unwrapped = 10
432
+ const createUser = (email: string): StrictResult<User, CreateUserError> =>
433
+ validateEmail(email)
434
+ .andThen(ensureUserDoesNotExist)
435
+ .andThen(insertUser)
1037
436
  ```
1038
437
 
1039
- [⬆️ Back to top](#toc)
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
- ```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`)
440
+ Use `catchTag` when one tagged error can be recovered locally:
1059
441
 
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
- }
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
- **Example**
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
- // 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
- }
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
- [⬆️ 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
- ```
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
- **Example:**
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
- 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)
1152
-
1153
- ---
1154
-
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 {
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
- **Example:**
1168
- ```typescript
490
+ If a handler is missing, TypeScript reports it. For partial boundary matching, use
491
+ `matchTagsPartial(okHandler, handlers, fallback)`:
1169
492
 
1170
- const reader = ok(file) // content file is line 01
1171
-
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
- })
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
- #### `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
- ```
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
- **Example:**
1201
- ```typescript
1202
- const reader = ok(file) // content file is line 01
516
+ ```ts
517
+ declare const error: InvalidEmailError | DatabaseError | Error
1203
518
 
1204
- const result = reader.map(it => it.content).finally(_ => {
1205
- file.close()
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
- #### `ResultAsync.orElse` (method)
526
+ ## Partial Matching
1218
527
 
1219
- Takes an `Err` value and maps it to a `ResultAsync<T, SomeNewType>`. This is useful for error recovery.
528
+ Use `matchErrorPartial` when only some tagged errors need special handling.
1220
529
 
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
- ```
1230
-
1231
- [⬆️ Back to top](#toc)
1232
-
1233
- ---
530
+ ```ts
531
+ import { matchErrorPartial } from 'resultar'
1234
532
 
1235
- #### `ResultAsync.tap` (method)
533
+ declare const error: InvalidEmailError | UserAlreadyExistsError | DatabaseError
1236
534
 
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
- }
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
- **Example:**
1247
- See [`Result.tap` (method)](#resulttap-method)
1248
-
544
+ ## Causes
1249
545
 
1250
- [⬆️ Back to top](#toc)
546
+ Tagged errors support native `cause` and cause-chain lookup.
1251
547
 
1252
- ---
548
+ ```ts
549
+ import { findCause } from 'resultar'
1253
550
 
1254
- #### `ResultAsync.tapError` (method)
551
+ const cause = new Error('connection refused')
552
+ const error = new DatabaseError({
553
+ cause,
554
+ operation: 'insert-user',
555
+ })
1255
556
 
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
- }
557
+ error.cause // cause
558
+ findCause(error, Error) // cause
559
+ error.findCause(Error) // cause
1263
560
  ```
1264
561
 
1265
- **Example:**
1266
- See [`Result.tapError` (method)](#resulttaperror-method)
1267
-
562
+ Cause lookup is cycle-safe: circular cause chains stop and return `undefined` when no matching cause
563
+ exists.
1268
564
 
1269
- [⬆️ Back to top](#toc)
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
- #### `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
- ```
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
- [⬆️ Back to top](#toc)
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
- #### `ResultAsync.match` (method)
580
+ const loadPort = (): Result<number, Error> =>
581
+ safeTry(function* () {
582
+ const config = yield* readConfig()
583
+ const port = yield* parsePort(config.port)
1305
584
 
1306
- Given 2 functions (one for the `Ok` variant and one for the `Err` variant) execute the function that matches the `ResultAsync` variant.
585
+ return ok(port)
586
+ })
1307
587
 
1308
- The difference with `Result.match` is that it always returns a `Promise` because of the asynchronous nature of the `ResultAsync`.
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
- **Signature:**
593
+ yield* persistPort(port)
1311
594
 
1312
- ```typescript
1313
- class ResultAsync<T, E> {
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
- **Example:**
599
+ ## Side Effects
1322
600
 
1323
- ```typescript
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
- import { validateUser } from 'imaginary-validator'
1326
- import { insertUser } from 'imaginary-database'
604
+ `tap(fn)` runs only for `Ok` and returns the original result unchanged.
1327
605
 
1328
- // ^ assume validateUser and insertUser have the following signatures:
1329
- // validateUser(user: User): Result<User, Error>
1330
- // insertUser(user): ResultAsync<User, Error>
606
+ ```ts
607
+ import { ok } from 'resultar'
1331
608
 
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
- )
609
+ const result = ok({ id: 'usr_123' }).tap((user) => {
610
+ console.info('user loaded', user.id)
611
+ })
1339
612
 
1340
- // resultMessage is a string
613
+ // still Result<{ id: string }, never>
614
+ result
1341
615
  ```
1342
616
 
1343
- [⬆️ Back to top](#toc)
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
- 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
- ```
619
+ ```ts
620
+ import { err } from 'resultar'
1372
621
 
1373
- Example:
1374
- ```typescript
1375
- const resultList: ResultAsync<number, never>[] =
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
- const combinedList: ResultAsync<number[], unknown> =
1379
- ResultAsync.combine(resultList)
626
+ // still Result<never, Error>
627
+ result
1380
628
  ```
1381
629
 
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
630
+ `log(fn)` runs for both states. For `Ok`, the callback receives `(value, undefined)`. For `Err`, it
631
+ receives `(undefined, error)`.
1386
632
 
1387
- const resultTuple: [ResultAsync<string, never>, ResultAsync<string, never>] =
1388
- tuple(okAsync('a'), okAsync('b'))
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
- const combinedTuple: ResultAsync<[string, string], unknown> =
1391
- ResultAsync.combine(resultTuple)
640
+ console.info('find user succeeded', user)
641
+ })
1392
642
  ```
1393
- [⬆️ Back to top](#toc)
1394
-
1395
- ---
1396
643
 
1397
- #### `ResultAsync.combineWithAllErrors` (static class method)
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
- 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
-
1403
- Function signature:
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
- Example usage:
1417
-
1418
- ```typescript
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
- const result = ResultAsync.combineWithAllErrors(resultList)
1427
-
1428
- // result is Err(['boooom!', 'ahhhhh!'])
658
+ ```ts
659
+ const result = findUser('usr_123').finally((_user, _error) => {
660
+ span.end()
661
+ })
1429
662
  ```
1430
663
 
1431
- #### `ResultAsync.safeUnwrap()`
1432
-
1433
- Allows for unwrapping a `Result` or returning an `Err` implicitly, thereby reducing boilerplate.
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
- [⬆️ Back to top](#toc)
669
+ ## Coming From Other Styles
1436
670
 
1437
- #### `ResultAsync.tryCatch()` (static class method)
671
+ ### From try/catch
1438
672
 
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.
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
- **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
- }
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
- 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)
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
- 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
- }
797
+ expect(ok(42)._unsafeUnwrap()).toBe(42)
1611
798
  ```
1612
799
 
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
- ---
800
+ By default, thrown unwrap errors omit stack traces for cleaner test output. Pass `withStackTrace` if needed:
1618
801
 
1619
- ### Testing
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
- 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:
806
+ ## Development
1634
807
 
1635
- ```typescript
1636
- import { ok } from 'resultar'
808
+ This repository uses Vite+.
1637
809
 
1638
- // ...
1639
-
1640
- expect(callSomeFunctionThatReturnsAResult("with", "some", "args")).toEqual(ok(someExpectation));
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
- 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
- ```
820
+ Package output:
1652
821
 
1653
- ---
822
+ - `dist/index.js`
823
+ - `dist/index.js.map`
824
+ - `dist/index.d.ts`
1654
825
 
1655
- ### License
826
+ ## License
1656
827
 
1657
- The resultar project is available as open source under the terms of the [MIT license](https://github.com/inaiat/resultar/blob/master/LICENSE).
828
+ MIT