resultar 2.0.0 → 3.0.0-alpha.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,12 +1,10 @@
1
1
  # Resultar
2
2
 
3
- Typed `Result` primitives for TypeScript, with ergonomic tagged errors.
3
+ Production-facing error handling for TypeScript, without a runtime.
4
4
 
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.
8
-
9
- Resultar models fallible work explicitly:
5
+ Resultar gives you typed `Result` primitives, real `Error`-based tagged failures, sync/async
6
+ composition, and optional no-discard diagnostics. It is small enough to adopt in one module and
7
+ strict enough to use at service, HTTP, job, queue, CLI, and integration boundaries.
10
8
 
11
9
  ```ts
12
10
  Result<T, E>
@@ -15,22 +13,18 @@ StrictResult<T, E extends Error>
15
13
  StrictResultAsync<T, E extends Error>
16
14
  ```
17
15
 
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.
22
-
23
- ## Requirements
24
-
25
- - ESM only
26
- - Node.js 24+
27
- - TypeScript 6-ready
16
+ Use it when expected failures should be impossible to miss:
28
17
 
29
- ```ts
30
- import { ok, err, createTaggedError } from 'resultar'
31
- ```
18
+ - Function signatures show both the success type and the expected error type.
19
+ - Domain errors are real `Error` instances with stable tags, messages, `cause`, stack traces, and
20
+ JSON output.
21
+ - Sync and async fallible work compose without `try/catch` blocks scattered through application
22
+ code.
23
+ - Ignored `Result` values can be reported by a type-aware no-discard check.
32
24
 
33
- CommonJS `require('resultar')` is not exported.
25
+ Resultar began as an initial fork of `neverthrow`. The v3 alpha line keeps the explicit wrapper
26
+ model, then leans into Resultar-specific tagged errors, strict service-boundary types, TypeScript 6
27
+ support, and ESM-only packaging.
34
28
 
35
29
  ## Install
36
30
 
@@ -42,138 +36,40 @@ pnpm add resultar
42
36
  npm install resultar
43
37
  ```
44
38
 
45
- ## V2 Deprecation Notice
46
-
47
- The `2.0.0` line removes deprecated APIs before stable release:
48
-
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* () { ... })` |
55
-
56
- `Result#safeUnwrap()` remains available for compatibility, but new code should prefer `yield* result`.
57
-
58
- ## Philosophy
59
-
60
- Resultar is a small explicit-result library, not an application runtime or an Effect-style framework.
61
- It focuses on three things:
62
-
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.
66
-
67
- The core model stays intentionally simple:
68
-
69
- ```ts
70
- Result<T, E>
71
- ResultAsync<T, E>
72
- ```
73
-
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.
77
-
78
- ## Recommended Production Style
79
-
80
- For backend and application code, prefer this stack:
39
+ ## Requirements
81
40
 
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.
41
+ - Node.js 24+
42
+ - TypeScript 6+
43
+ - ESM only
87
44
 
88
45
  ```ts
89
- import type { StrictResult } from 'resultar'
90
-
91
46
  import { createTaggedError, ok } from 'resultar'
92
-
93
- class UserAlreadyExistsError extends createTaggedError({
94
- name: 'UserAlreadyExistsError',
95
- message: 'User $email already exists',
96
- }) {}
97
-
98
- const ensureUserDoesNotExist = (
99
- email: string,
100
- ): StrictResult<string, UserAlreadyExistsError> =>
101
- email === 'taken@example.com' ? UserAlreadyExistsError.err({ email }) : ok(email)
102
- ```
103
-
104
- Avoid using strings or loose objects for production-facing expected failures:
105
-
106
- ```ts
107
- // Local-only style. Avoid at service boundaries.
108
- type ParseError = 'InvalidPort'
109
- ```
110
-
111
- ### No-Discard Check
112
-
113
- Resultar ships a type-aware check for discarded `Result` and `ResultAsync` values:
114
-
115
- ```json
116
- {
117
- "scripts": {
118
- "lint:resultar": "resultar-no-discard --project tsconfig.json"
119
- }
120
- }
47
+ import type { StrictResult } from 'resultar'
121
48
  ```
122
49
 
123
- It fails code that ignores a returned result:
50
+ CommonJS `require('resultar')` is not exported.
124
51
 
125
- ```ts
126
- saveUser(input)
127
- ```
52
+ ## Why Teams Use It
128
53
 
129
- Handle the result, return it, or mark an intentional discard with `void`:
54
+ This signature hides an expected failure:
130
55
 
131
56
  ```ts
132
- const result = saveUser(input)
133
- return saveUser(input)
134
- void saveUser(input)
135
- ```
136
-
137
- For Oxlint users, run it next to Oxlint because this rule needs TypeScript return-type information:
57
+ const parsePort = (value: string): number => {
58
+ const port = Number(value)
138
59
 
139
- ```json
140
- {
141
- "scripts": {
142
- "lint": "oxlint && resultar-no-discard --project tsconfig.json"
60
+ if (!Number.isInteger(port) || port <= 0) {
61
+ throw new Error(`Invalid port ${value}`)
143
62
  }
63
+
64
+ return port
144
65
  }
145
66
  ```
146
67
 
147
- ## API Decision Guide
148
-
149
- Use `map` when the `Ok` value changes and the transform cannot fail.
150
-
151
- Use `andThen` when the next step can fail and returns another `Result`. Use `asyncAndThen` when a
152
- sync `Result` continues into `ResultAsync`.
153
-
154
- Use `mapErr` when the error value changes. Use `orElse` when an error should recover into another
155
- `Result`.
156
-
157
- Use `catchTag` or `catchTags` for local recovery from tagged errors. `catchTags` is intentionally
158
- partial: unhandled tags stay in the error channel.
159
-
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.
163
-
164
- Use `fromPromise(promise, toError)` when you already have a promise. Use
165
- `tryCatchAsync(() => promiseFactory(), toError)` when creating the promise can throw synchronously.
166
-
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.
170
-
171
- ## Quick Start
68
+ This signature makes the failure part of the contract:
172
69
 
173
70
  ```ts
174
- import type { StrictResult } from 'resultar'
175
-
176
71
  import { createTaggedError, ok } from 'resultar'
72
+ import type { StrictResult } from 'resultar'
177
73
 
178
74
  class InvalidPortError extends createTaggedError({
179
75
  name: 'InvalidPortError',
@@ -183,163 +79,134 @@ class InvalidPortError extends createTaggedError({
183
79
  const parsePort = (value: string): StrictResult<number, InvalidPortError> => {
184
80
  const port = Number(value)
185
81
 
186
- return Number.isInteger(port) && port > 0
187
- ? ok(port)
188
- : InvalidPortError.err({ value })
82
+ return Number.isInteger(port) && port > 0 ? ok(port) : InvalidPortError.err({ value })
189
83
  }
190
-
191
- const message = parsePort('3000').match(
192
- (port) => `Listening on ${port}`,
193
- (error) => error.message,
194
- )
195
84
  ```
196
85
 
197
- ## Strict Results
198
-
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.
201
-
202
- ```ts
203
- import type { StrictResult } from 'resultar'
204
-
205
- import { createTaggedError, ok } from 'resultar'
206
-
207
- class InvalidEmailError extends createTaggedError({
208
- name: 'InvalidEmailError',
209
- message: 'Invalid email $email',
210
- }) {}
86
+ Callers now know the success type and the expected error type without reading the implementation.
87
+ That is the main trade: expected failures become visible contracts instead of tribal knowledge.
211
88
 
212
- const validateEmail = (email: string): StrictResult<string, InvalidEmailError> =>
213
- email.includes('@') ? ok(email) : InvalidEmailError.err({ email })
214
- ```
89
+ Resultar is intentionally not an application runtime. It gives you explicit error values that work
90
+ in normal TypeScript.
215
91
 
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.
92
+ ## What You Get
219
93
 
220
- ## Expected Failure Style
94
+ | Problem | Resultar gives you |
95
+ | --- | --- |
96
+ | Expected failures are hidden behind `throw` | `Result<T, E>` and `ResultAsync<T, E>` in function signatures |
97
+ | Production errors need stable names and metadata | `createTaggedError` classes with `_tag`, template props, `cause`, and `.toJSON()` |
98
+ | Async flows turn into nested `try/catch` | `tryResultAsync`, `andThen`, `asyncAndThen`, `map`, `orElse`, `safeTry` |
99
+ | Boundary handlers miss cases | `matchTags` for exhaustive tagged-error handling |
100
+ | Results are easy to ignore | `resultar-no-discard` and the `resultar-ls` TypeScript plugin |
101
+ | You do not want another application runtime | A small ESM library around explicit values |
221
102
 
222
- For expected application and domain failures, prefer `createTaggedError` classes and return them
223
- through `StrictResult` or `StrictResultAsync`.
103
+ Nested `try/catch` often masks the error the caller actually needs:
224
104
 
225
105
  ```ts
226
- class UserAlreadyExistsError extends createTaggedError({
227
- name: 'UserAlreadyExistsError',
228
- message: 'User $email already exists',
229
- }) {}
230
- ```
106
+ const getPosts = async (url: string) => {
107
+ try {
108
+ const response = await fetch(url)
231
109
 
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.
110
+ if (response.status >= 400) {
111
+ throw new HttpClientError(response.status)
112
+ }
236
113
 
237
- ## Result
114
+ return response.json() as Promise<Post[]>
115
+ } catch (cause) {
116
+ throw new InfrastructureError({ cause })
117
+ }
118
+ }
119
+ ```
238
120
 
239
- `Result<T, E>` is either `Ok<T>` or `Err<E>`.
121
+ Resultar keeps each failure explicit and typed:
240
122
 
241
123
  ```ts
242
- import type { StrictResult } from 'resultar'
243
-
244
- import { createTaggedError, ok } from 'resultar'
245
-
246
- class UserNotFoundError extends createTaggedError({
247
- name: 'UserNotFoundError',
248
- message: 'User $id not found',
249
- }) {}
250
-
251
- const findUser = (id: string): StrictResult<{ id: string }, UserNotFoundError> =>
252
- id === 'usr_123' ? ok({ id }) : UserNotFoundError.err({ id })
253
-
254
- const result = findUser('usr_123')
255
-
256
- if (result.isOk()) {
257
- result.value.id
258
- }
259
-
260
- if (result.isErr()) {
261
- result.error.message
262
- }
124
+ const getPosts = (
125
+ url: string,
126
+ ): StrictResultAsync<Post[], FetchPostsError | HttpClientError | ParsePostsError> =>
127
+ tryResultAsync(fetch(url), (cause) => new FetchPostsError({ cause }))
128
+ .andThen((response) =>
129
+ response.status >= 400 ? HttpClientError.err({ status: response.status }) : ok(response),
130
+ )
131
+ .andThen((response) =>
132
+ tryResultAsync(response.json() as Promise<Post[]>, (cause) => new ParsePostsError({ cause })),
133
+ )
263
134
  ```
264
135
 
265
- ### Chaining
266
-
267
- Use `map` for infallible transforms and `andThen` for fallible transforms.
136
+ ## Quick Start
268
137
 
269
138
  ```ts
270
- import type { StrictResult } from 'resultar'
271
-
272
139
  import { createTaggedError, ok } from 'resultar'
273
-
274
- type User = {
275
- readonly email: string
276
- }
140
+ import type { StrictResult } from 'resultar'
277
141
 
278
142
  class InvalidEmailError extends createTaggedError({
279
143
  name: 'InvalidEmailError',
280
144
  message: 'Invalid email $email',
281
145
  }) {}
282
146
 
283
- class InvalidDomainError extends createTaggedError({
284
- name: 'InvalidDomainError',
285
- message: 'Invalid email domain $domain',
147
+ class UserAlreadyExistsError extends createTaggedError({
148
+ name: 'UserAlreadyExistsError',
149
+ message: 'User $email already exists',
286
150
  }) {}
287
151
 
152
+ type User = {
153
+ readonly email: string
154
+ readonly id: string
155
+ }
156
+
288
157
  const validateEmail = (email: string): StrictResult<string, InvalidEmailError> =>
289
158
  email.includes('@') ? ok(email) : InvalidEmailError.err({ email })
290
159
 
291
- const createUser = (email: string): StrictResult<User, InvalidEmailError> =>
292
- validateEmail(email).map((validEmail) => ({ email: validEmail }))
293
- ```
294
-
295
- Use `filterOrElse` when a successful value must satisfy an extra predicate:
296
-
297
- ```ts
298
- const validateCompanyEmail = (
160
+ const ensureUserDoesNotExist = (
299
161
  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
- )
305
- ```
306
-
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`.
162
+ ): StrictResult<string, UserAlreadyExistsError> =>
163
+ email === 'taken@example.com' ? UserAlreadyExistsError.err({ email }) : ok(email)
310
164
 
311
- Use `pipe` when you want to apply reusable Result combinators:
165
+ const insertUser = (email: string): StrictResult<User, never> =>
166
+ ok({ email, id: 'usr_123' })
312
167
 
313
- ```ts
314
- const audit =
315
- <T, E>(result: Result<T, E>): Result<T, E> =>
316
- result.tap((value) => console.log('created', value))
168
+ const createUser = (
169
+ email: string,
170
+ ): StrictResult<User, InvalidEmailError | UserAlreadyExistsError> =>
171
+ validateEmail(email).andThen(ensureUserDoesNotExist).andThen(insertUser)
317
172
 
318
- const result = createUser('person@example.com').pipe(
319
- audit,
320
- (userResult) => userResult.map((user) => user.email),
173
+ const response = createUser('taken@example.com').matchTags(
174
+ (user) => ({ body: user, statusCode: 201 }),
175
+ {
176
+ InvalidEmailError: (error) => ({
177
+ body: { code: error._tag, message: error.message },
178
+ statusCode: 400,
179
+ }),
180
+ UserAlreadyExistsError: (error) => ({
181
+ body: { code: error._tag, message: error.message },
182
+ statusCode: 409,
183
+ }),
184
+ },
321
185
  )
322
186
  ```
323
187
 
324
- ### Matching
188
+ ## Core Model
325
189
 
326
- Use `match` at boundaries.
190
+ `Result<T, E>` is a synchronous fallible value. It is either an `Ok<T>` or an `Err<E>`.
327
191
 
328
192
  ```ts
329
- const response = createUser('person@example.com').match(
330
- (user) => ({ body: user, statusCode: 201 }),
331
- (error) => ({ body: { error }, statusCode: 400 }),
332
- )
333
- ```
193
+ const result = parsePort('3000')
334
194
 
335
- ## ResultAsync
195
+ if (result.isOk()) {
196
+ result.value
197
+ }
336
198
 
337
- `ResultAsync<T, E>` wraps `Promise<Result<T, E>>` and keeps the same composition style.
199
+ if (result.isErr()) {
200
+ result.error
201
+ }
202
+ ```
338
203
 
339
- ```ts
340
- import type { StrictResultAsync } from 'resultar'
204
+ `ResultAsync<T, E>` is the async version. It wraps promise-based fallible work while keeping the
205
+ same composition API.
341
206
 
342
- import { createTaggedError, fromPromise } from 'resultar'
207
+ ```ts
208
+ import { createTaggedError, fromPromise, ok, tryResultAsync } from 'resultar'
209
+ import type { StrictResult, StrictResultAsync } from 'resultar'
343
210
 
344
211
  type User = {
345
212
  readonly id: string
@@ -350,11 +217,48 @@ class FetchUserError extends createTaggedError({
350
217
  message: 'Failed to fetch user $id',
351
218
  }) {}
352
219
 
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
- )
220
+ class UserResponseError extends createTaggedError({
221
+ name: 'UserResponseError',
222
+ message: 'User request $id failed with status $status',
223
+ }) {}
224
+
225
+ class ParseUserError extends createTaggedError({
226
+ name: 'ParseUserError',
227
+ message: 'Failed to parse user $id response',
228
+ }) {}
229
+
230
+ class DecodeUserError extends createTaggedError({
231
+ name: 'DecodeUserError',
232
+ message: 'Invalid user $id response payload',
233
+ }) {}
234
+
235
+ const decodeUser = (input: unknown, id: string): StrictResult<User, DecodeUserError> => {
236
+ if (
237
+ typeof input === 'object' &&
238
+ input !== null &&
239
+ 'id' in input &&
240
+ typeof input.id === 'string'
241
+ ) {
242
+ return ok({ id: input.id })
243
+ }
244
+
245
+ return DecodeUserError.err({ id })
246
+ }
247
+
248
+ const fetchUser = (
249
+ id: string,
250
+ ): StrictResultAsync<User, FetchUserError | UserResponseError | ParseUserError | DecodeUserError> =>
251
+ fromPromise(fetch(`https://example.com/users/${id}`), (cause) => new FetchUserError({ cause, id }))
252
+ .andThen((response) =>
253
+ response.ok ? ok(response) : UserResponseError.err({ id, status: response.status }),
254
+ )
255
+ .andThen((response) =>
256
+ tryResultAsync<unknown, ParseUserError>(
257
+ () => response.json(),
258
+ (cause) => new ParseUserError({ cause, id }),
259
+ ),
260
+ )
261
+ .andThen((input) => decodeUser(input, id))
358
262
 
359
263
  const label = await fetchUser('usr_123').match(
360
264
  (user) => `User ${user.id}`,
@@ -362,19 +266,19 @@ const label = await fetchUser('usr_123').match(
362
266
  )
363
267
  ```
364
268
 
365
- ## Tagged Errors
269
+ This keeps network failures, non-2xx responses, JSON parse failures, and payload decode failures
270
+ separate. In production, treat parsing bytes into `unknown` and decoding `unknown` into a domain
271
+ type as different failure modes.
366
272
 
367
- Tagged errors are real `Error` subclasses with:
273
+ Use generic `Result<T, E>` for local flows where strings, enums, or small domain objects are useful
274
+ as `E`. Prefer `StrictResult<T, E extends Error>` and
275
+ `StrictResultAsync<T, E extends Error>` at service, HTTP, job, queue, CLI, and integration
276
+ boundaries.
368
277
 
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)`
278
+ ## Tagged Errors
279
+
280
+ `createTaggedError` creates real `Error` subclasses with stable tags and typed message-template
281
+ props.
378
282
 
379
283
  ```ts
380
284
  import { createTaggedError } from 'resultar'
@@ -397,7 +301,19 @@ error.id // string | number
397
301
  error.source // string | number
398
302
  ```
399
303
 
400
- Missing template props are TypeScript errors:
304
+ Tagged errors include:
305
+
306
+ - `_tag`
307
+ - `messageTemplate`
308
+ - `fingerprint`
309
+ - `cause`
310
+ - `.toJSON()`
311
+ - `.findCause(ErrorClass)`
312
+ - static `.is(value)` for nominal checks
313
+ - static `.err(props)` for returning `Err`
314
+
315
+ Template variables are inferred from `$variables` in the message. Missing variables are TypeScript
316
+ errors.
401
317
 
402
318
  ```ts
403
319
  new UserNotFoundError({ id: 'usr_123', source: 'database' })
@@ -406,18 +322,7 @@ new UserNotFoundError({ id: 'usr_123', source: 'database' })
406
322
  new UserNotFoundError({ id: 'usr_123' })
407
323
  ```
408
324
 
409
- Reserved template variables are rejected because they would conflict with `Error` or tagged-error
410
- metadata:
411
-
412
- ```ts
413
- // @ts-expect-error name is reserved
414
- createTaggedError({ name: 'BadError', message: 'Bad $name' })
415
- ```
416
-
417
- Reserved variables are `_tag`, `name`, `message`, `messageTemplate`, `fingerprint`, `stack`, and
418
- `cause`.
419
-
420
- For lightweight tagged domain unions that do not need to extend `Error`, use `TaggedEnum`:
325
+ Use `TaggedEnum` only for lightweight tagged unions that do not need to be real errors.
421
326
 
422
327
  ```ts
423
328
  import type { TaggedEnum } from 'resultar'
@@ -426,435 +331,365 @@ type PaymentError = TaggedEnum<{
426
331
  CardDeclined: { readonly code: string }
427
332
  InsufficientFunds: { readonly balance: number }
428
333
  }>
429
-
430
- const error: PaymentError = { _tag: 'CardDeclined', code: 'card_declined' }
431
334
  ```
432
335
 
433
- ## Tagged Errors With Result
336
+ ## Composing Results
434
337
 
435
- Tagged errors are most useful as the `E` side of `StrictResult<T, E>` and
436
- `StrictResultAsync<T, E>`.
338
+ Use `map` for infallible transforms:
437
339
 
438
340
  ```ts
439
- import type { StrictResult } from 'resultar'
440
-
441
- import { createTaggedError, ok } from 'resultar'
442
-
443
- class InvalidEmailError extends createTaggedError({
444
- name: 'InvalidEmailError',
445
- message: 'Invalid email $email',
446
- }) {}
447
-
448
- class UserAlreadyExistsError extends createTaggedError({
449
- name: 'UserAlreadyExistsError',
450
- message: 'User $email already exists',
451
- }) {}
452
-
453
- class DatabaseError extends createTaggedError({
454
- name: 'DatabaseError',
455
- message: 'Database operation $operation failed',
456
- }) {}
457
-
458
- type User = {
459
- readonly email: string
460
- readonly id: string
461
- }
462
-
463
- type CreateUserError = InvalidEmailError | UserAlreadyExistsError | DatabaseError
464
-
465
- const validateEmail = (email: string): StrictResult<string, InvalidEmailError> =>
466
- email.includes('@') ? ok(email) : InvalidEmailError.err({ email })
341
+ const normalized = validateEmail('PERSON@EXAMPLE.COM').map((email) => email.toLowerCase())
342
+ ```
467
343
 
468
- const ensureUserDoesNotExist = (
469
- email: string,
470
- ): StrictResult<string, UserAlreadyExistsError | DatabaseError> => {
471
- if (email === 'taken@example.com') {
472
- return UserAlreadyExistsError.err({ email })
473
- }
344
+ Use `as` when success matters but the previous value does not:
474
345
 
475
- return ok(email)
476
- }
346
+ ```ts
347
+ const created = insertUser(user).as({ status: 'created' as const })
348
+ ```
477
349
 
478
- const insertUser = (email: string): StrictResult<User, DatabaseError> =>
479
- ok({ email, id: 'usr_123' })
350
+ Use `andThen` when the next step can fail:
480
351
 
481
- const createUser = (email: string): StrictResult<User, CreateUserError> =>
482
- validateEmail(email)
483
- .andThen(ensureUserDoesNotExist)
484
- .andThen(insertUser)
352
+ ```ts
353
+ const user = validateEmail(email).andThen(ensureUserDoesNotExist).andThen(insertUser)
485
354
  ```
486
355
 
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>`.
488
-
489
- Use `catchTag` when one tagged error can be recovered locally:
356
+ Use `asyncAndThen` when a sync result continues into async work:
490
357
 
491
358
  ```ts
492
- const result = createUser('bad-email').catchTag('InvalidEmailError', (error) =>
493
- ok({
494
- email: error.email,
495
- id: 'draft_user',
496
- }),
497
- )
359
+ const user = validateEmail(email).asyncAndThen(ensureUserDoesNotExistAsync).andThen(insertUserAsync)
498
360
  ```
499
361
 
500
- Use `catchTags` when a small set of tagged errors share local recovery:
362
+ Use `filterOrElse` when an `Ok` value must satisfy another predicate:
501
363
 
502
364
  ```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' }),
506
- })
507
- ```
365
+ class InvalidDomainError extends createTaggedError({
366
+ name: 'InvalidDomainError',
367
+ message: 'Invalid email domain $domain',
368
+ }) {}
508
369
 
509
- `catchTags` is intentionally partial: any unhandled error tag stays in the `Err` side. A separate
510
- `partialCatchTags` API would duplicate this behavior.
370
+ const validateCompanyEmail = (
371
+ email: string,
372
+ ): StrictResult<string, InvalidEmailError | InvalidDomainError> =>
373
+ validateEmail(email).filterOrElse(
374
+ (validEmail) => validEmail.endsWith('@company.com'),
375
+ (validEmail) => new InvalidDomainError({ domain: validEmail.split('@')[1] ?? 'unknown' }),
376
+ )
377
+ ```
511
378
 
512
- At the boundary, use `matchTags` for direct success and tagged-error response mapping:
379
+ Use `catchTag` or `catchTags` for local recovery from tagged errors:
513
380
 
514
381
  ```ts
515
- const result = createUser('taken@example.com')
516
-
517
- const response = result.matchTags(
518
- (user) => ({
519
- body: user,
520
- statusCode: 201,
382
+ const draftUser = createUser('bad-email').catchTag('InvalidEmailError', (error) =>
383
+ ok({
384
+ email: error.email,
385
+ id: 'draft_user',
521
386
  }),
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
387
  )
537
388
  ```
538
389
 
539
- If a handler is missing, TypeScript reports it. For partial boundary matching, use
540
- `matchTagsPartial(okHandler, handlers, fallback)`:
390
+ Use `match`, `matchTags`, or `matchTagsPartial` at boundaries where a result becomes a response,
391
+ log entry, queue acknowledgement, CLI exit code, or UI state.
541
392
 
542
393
  ```ts
543
- const response = result.matchTagsPartial(
544
- (user) => ({
545
- body: user,
546
- statusCode: 201,
547
- }),
394
+ const response = createUser(input.email).matchTagsPartial(
395
+ (user) => ({ body: user, statusCode: 201 }),
548
396
  {
549
- InvalidEmailError: (err) => ({
550
- body: { code: err._tag, message: err.message },
397
+ InvalidEmailError: (error) => ({
398
+ body: { code: error._tag, message: error.message },
551
399
  statusCode: 400,
552
400
  }),
553
401
  },
554
- (err) => ({
555
- body: { code: 'UnhandledError', message: err.message },
402
+ (error) => ({
403
+ body: { code: 'UnhandledError', message: error.message },
556
404
  statusCode: 500,
557
405
  }),
558
406
  )
559
407
  ```
560
408
 
561
- Use `catchTags` for pipeline recovery and `matchTagsPartial` for boundary mapping.
562
-
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`.
409
+ ## Fallbacks
564
410
 
565
- ```ts
566
- declare const error: InvalidEmailError | DatabaseError | Error
567
-
568
- const message = matchError(error, {
569
- DatabaseError: (err) => err.message,
570
- Error: (err) => err.message,
571
- InvalidEmailError: (err) => err.message,
572
- })
573
- ```
411
+ Use fallback APIs when an expected failure has a useful recovery path.
574
412
 
575
- ## Partial Matching
413
+ | Effect fallback idea | Resultar equivalent |
414
+ | --- | --- |
415
+ | try another effect | `result.orElse(() => fallbackResult)` |
416
+ | replace the failure | `mapErr(...)` or `orElse(() => err(newError))` |
417
+ | replace the failure with success | `orElse(() => ok(defaultValue))` inside a pipeline |
418
+ | default at the edge | `unwrapOr(defaultValue)` |
419
+ | try ordered candidates | `Result.firstSuccessOf` or `ResultAsync.firstSuccessOf` |
576
420
 
577
- Use `matchErrorPartial` when only some tagged errors need special handling.
421
+ Use `orElse` for pipeline recovery:
578
422
 
579
423
  ```ts
580
- import { matchErrorPartial } from 'resultar'
424
+ const user = readUserFromCache(id).orElse(() => readUserFromRemote(id))
425
+ ```
581
426
 
582
- declare const error: InvalidEmailError | UserAlreadyExistsError | DatabaseError
427
+ Use `mapErr` when a low-level failure should become a domain failure:
583
428
 
584
- const message = matchErrorPartial(
585
- error,
586
- {
587
- InvalidEmailError: (err) => `Invalid input: ${err.email}`,
588
- },
589
- (err) => err.message,
429
+ ```ts
430
+ const user = readUserFromRemote(id).mapErr(
431
+ (cause) => new ReadUserError({ cause, id }),
590
432
  )
591
433
  ```
592
434
 
593
- ## Causes
594
-
595
- Tagged errors support native `cause` and cause-chain lookup.
435
+ Use `orElse(() => ok(defaultValue))` when fallback success should stay inside the pipeline:
596
436
 
597
437
  ```ts
598
- import { findCause } from 'resultar'
438
+ const user = readUser(id).orElse(() =>
439
+ ok({
440
+ email: 'guest@example.com',
441
+ id: 'guest',
442
+ }),
443
+ )
444
+ ```
599
445
 
600
- const cause = new Error('connection refused')
601
- const error = new DatabaseError({
602
- cause,
603
- operation: 'insert-user',
604
- })
446
+ Use `firstSuccessOf` when each fallback should run only if earlier candidates fail:
605
447
 
606
- error.cause // cause
607
- findCause(error, Error) // cause
608
- error.findCause(Error) // cause
448
+ ```ts
449
+ const user = ResultAsync.firstSuccessOf([
450
+ () => readUserFromCache(id),
451
+ () => readUserFromPrimary(id),
452
+ () => readUserFromReplica(id),
453
+ ])
609
454
  ```
610
455
 
611
- Cause lookup is cycle-safe: circular cause chains stop and return `undefined` when no matching cause
612
- exists.
613
-
614
- ## Safe Try
456
+ Use `catchTag` / `catchTags` when recovery depends on a specific tagged error. Use
457
+ `matchTagsPartial` when only a boundary needs fallback mapping. Use `unwrapOr` only at final edges
458
+ where defaulting is intentional.
615
459
 
616
- `safeTry` lets you unwrap successful results inside a generator and return early on the first error.
460
+ ## Wrapping Throwing Code
617
461
 
618
- The shortest form is `yield* result`. `yield* result.safeUnwrap()` is still supported, but usually not necessary.
462
+ Use `tryResult` and `tryResultAsync` at the edge of uncontrolled code:
463
+ JSON parsing, platform APIs, third-party libraries, I/O, and network calls.
619
464
 
620
465
  ```ts
621
- import type { Result, ResultAsync } from 'resultar'
622
-
623
- import { ok, safeTry } from 'resultar'
466
+ import { tryResult } from 'resultar'
624
467
 
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>
628
-
629
- const loadPort = (): Result<number, Error> =>
630
- safeTry(function* () {
631
- const config = yield* readConfig()
632
- const port = yield* parsePort(config.port)
633
-
634
- return ok(port)
635
- })
468
+ class ParseConfigError extends createTaggedError({
469
+ name: 'ParseConfigError',
470
+ message: 'Could not parse config',
471
+ }) {}
636
472
 
637
- const savePort = (): ResultAsync<number, Error> =>
638
- safeTry(async function* () {
639
- const config = yield* readConfig()
640
- const port = yield* parsePort(config.port)
473
+ const parseConfig = (input: string): StrictResult<Config, ParseConfigError> =>
474
+ tryResult(
475
+ () => JSON.parse(input) as Config,
476
+ (cause) => new ParseConfigError({ cause }),
477
+ )
478
+ ```
641
479
 
642
- yield* persistPort(port)
480
+ Use `fromPromise` when you already have a promise and need to map rejection into an expected error:
643
481
 
644
- return ok(port)
645
- })
482
+ ```ts
483
+ const user = fromPromise(loadUserFromRemote(id), (cause) => new FetchUserError({ cause, id }))
646
484
  ```
647
485
 
648
- ## Side Effects
486
+ After that edge conversion, keep your own domain functions returning `Result` values instead of
487
+ throwing expected failures.
649
488
 
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.
489
+ ## Unexpected Errors
652
490
 
653
- `tap(fn)` runs only for `Ok` and returns the original result unchanged.
491
+ Expected application failures belong in `Err`. Bugs, impossible states, and unrecoverable conditions
492
+ should remain normal JavaScript throws or rejected promises.
654
493
 
655
- ```ts
656
- import { ok } from 'resultar'
494
+ Use this boundary rule:
657
495
 
658
- const result = ok({ id: 'usr_123' }).tap((user) => {
659
- console.info('user loaded', user.id)
660
- })
496
+ - Convert uncontrolled throws and rejections into expected errors with `tryResult`, `tryResultAsync`,
497
+ `fromThrowable`, `fromThrowableAsync`, or `fromPromise`.
498
+ - Keep your own recoverable domain failures as `Err`.
499
+ - Use `unwrapOrThrow()` only at final edges where turning an `Err` into a thrown error is deliberate.
661
500
 
662
- // still Result<{ id: string }, never>
663
- result
501
+ ```ts
502
+ const user = await fetchUser('usr_123').unwrapOrThrow()
664
503
  ```
665
504
 
666
- `tapError(fn)` runs only for `Err` and returns the original result unchanged.
505
+ Inside `safeTry`, the object form can map thrown bugs from the generator body into an explicit error:
667
506
 
668
507
  ```ts
669
- import { err } from 'resultar'
670
-
671
- const result = err(new Error('database unavailable')).tapError((error) => {
672
- console.error('load failed', error)
508
+ const config = safeTry({
509
+ *try() {
510
+ const raw = yield* readConfigFile()
511
+ return ok(JSON.parse(raw) as Config)
512
+ },
513
+ catch: (cause) => new ParseConfigError({ cause }),
673
514
  })
674
-
675
- // still Result<never, Error>
676
- result
677
515
  ```
678
516
 
679
- `log(fn)` runs for both states. For `Ok`, the callback receives `(value, undefined)`. For `Err`, it
680
- receives `(undefined, error)`.
517
+ ## Linear Result Code
518
+
519
+ Use `safeTry` when a pipeline reads better as linear code. `yield*` unwraps the `Ok` value and
520
+ short-circuits on the first `Err`.
681
521
 
682
522
  ```ts
683
- const result = findUser('usr_123').log((user, error) => {
684
- if (error) {
685
- console.error('find user failed', error)
686
- return
687
- }
523
+ import { safeTry } from 'resultar'
688
524
 
689
- console.info('find user succeeded', user)
690
- })
525
+ const createUser = (
526
+ email: string,
527
+ ): StrictResult<User, InvalidEmailError | UserAlreadyExistsError> =>
528
+ safeTry(function* () {
529
+ const validEmail = yield* validateEmail(email)
530
+ const availableEmail = yield* ensureUserDoesNotExist(validEmail)
531
+
532
+ return insertUser(availableEmail)
533
+ })
691
534
  ```
692
535
 
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.
536
+ The same `safeTry` helper supports async generators:
695
537
 
696
538
  ```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 }))
539
+ const createUserAsync = (
540
+ email: string,
541
+ ): StrictResultAsync<User, InvalidEmailError | UserAlreadyExistsError | DatabaseError> =>
542
+ safeTry(async function* () {
543
+ const validEmail = yield* validateEmail(email)
544
+ const availableEmail = yield* ensureUserDoesNotExist(validEmail)
545
+
546
+ return insertUserAsync(availableEmail)
547
+ })
701
548
  ```
702
549
 
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.
550
+ The object form can map unexpected thrown errors while leaving yielded `Err` values unchanged:
705
551
 
706
552
  ```ts
707
- using result = findUser('usr_123').toDisposable((_user, _error) => {
708
- span.end()
709
- })
553
+ const config = safeTry({
554
+ *try() {
555
+ const raw = yield* readConfigFile()
710
556
 
711
- await using asyncResult = fetchUser('usr_123').toAsyncDisposable(async (_user, _error) => {
712
- await span.end()
557
+ return ok(JSON.parse(raw) as Config)
558
+ },
559
+ catch: (cause) => new ParseConfigError({ cause }),
713
560
  })
714
561
  ```
715
562
 
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.
563
+ ## No-Discard Validation
720
564
 
721
- ## Coming From Other Styles
565
+ Resultar values should not be ignored. Install `resultar-ls` to report discarded `Result` and
566
+ `ResultAsync` values.
722
567
 
723
- ### From try/catch
724
-
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.
727
-
728
- ```ts
729
- const parseConfig = (input: string): StrictResult<Config, ParseConfigError> =>
730
- tryCatch(
731
- () => JSON.parse(input) as Config,
732
- (cause) => new ParseConfigError({ cause }),
733
- )
568
+ ```sh
569
+ pnpm add -D resultar-ls typescript
734
570
  ```
735
571
 
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
572
+ Add a lint-like script:
755
573
 
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)`
574
+ ```json
575
+ {
576
+ "scripts": {
577
+ "lint:resultar": "resultar-no-discard --project tsconfig.json"
578
+ }
579
+ }
580
+ ```
802
581
 
803
- ### ResultAsync Helpers
582
+ This fails:
804
583
 
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
584
+ ```ts
585
+ saveUser(input)
586
+ ```
813
587
 
814
- ### ResultAsync Methods
588
+ These are intentional:
815
589
 
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()`
590
+ ```ts
591
+ const result = saveUser(input)
592
+ return saveUser(input)
593
+ void saveUser(input)
594
+ ```
829
595
 
830
- ### Tagged Error Helpers
596
+ For editor diagnostics, enable the TypeScript language-service plugin:
831
597
 
832
- - `createTaggedError(options)`
833
- - `matchError(error, handlers)`
834
- - `matchErrorPartial(error, handlers, fallback)`
835
- - `isError(value)`
836
- - `findCause(error, ErrorClass)`
837
- - `TaggedEnum<Members>` type helper
598
+ ```json
599
+ {
600
+ "compilerOptions": {
601
+ "plugins": [{ "name": "resultar-ls", "noDiscard": "error" }]
602
+ }
603
+ }
604
+ ```
838
605
 
839
- ## Testing
606
+ TypeScript language-service plugins are editor-only by default. To make `tsc` report Resultar
607
+ diagnostics during builds, patch the local TypeScript installation:
840
608
 
841
- `_unsafeUnwrap()` and `_unsafeUnwrapErr()` are intended for tests.
609
+ ```sh
610
+ pnpm exec resultar-ls patch
611
+ pnpm exec resultar-ls check
612
+ pnpm exec resultar-ls unpatch
613
+ ```
842
614
 
843
- ```ts
844
- import { ok } from 'resultar'
615
+ For TypeScript 7 native-preview projects, `resultar-tsgo` exposes a `tsgo` wrapper that runs native
616
+ TypeScript and then Resultar no-discard validation.
845
617
 
846
- expect(ok(42)._unsafeUnwrap()).toBe(42)
847
- ```
618
+ ## API Decision Guide
848
619
 
849
- By default, thrown unwrap errors omit stack traces for cleaner test output. Pass `withStackTrace` if needed:
850
-
851
- ```ts
852
- result._unsafeUnwrapErr({ withStackTrace: true })
853
- ```
854
-
855
- ## Development
856
-
857
- This repository uses Vite+.
620
+ | Need | Use |
621
+ | --- | --- |
622
+ | Create a sync success | `ok(value)` |
623
+ | Create a sync failure | `err(error)` or `ErrorClass.err(props)` |
624
+ | Create an async success | `okAsync(value)` |
625
+ | Create an async failure | `errAsync(error)` |
626
+ | Create an `Ok(undefined)` | `unit()` or `unitAsync()` |
627
+ | Convert throwing sync code | `tryResult` or `fromThrowable` |
628
+ | Convert rejecting async code | `tryResultAsync`, `fromPromise`, or `fromThrowableAsync` |
629
+ | Treat an `Err` as unrecoverable at an edge | `unwrapOrThrow()` |
630
+ | Transform success | `map` or `asyncMap` |
631
+ | Replace success with a known value | `as(value)` |
632
+ | Transform error | `mapErr` |
633
+ | Continue with fallible work | `andThen` or `asyncAndThen` |
634
+ | Recover from failure | `orElse`, `catchTag`, or `catchTags` |
635
+ | Replace failure with another failure | `mapErr` or `orElse(() => err(newError))` |
636
+ | Replace failure with success | `orElse(() => ok(defaultValue))` |
637
+ | Handle a boundary | `match`, `matchTags`, or `matchTagsPartial` |
638
+ | Write linear result code | `safeTry` |
639
+ | Combine two independent results | `Result.zip` or `ResultAsync.zip` |
640
+ | Combine many independent results | `Result.combine` or `ResultAsync.combine` |
641
+ | Collect all validation failures | `validateAll` or `combineWithAllErrors` |
642
+ | Try fallback candidates | `firstSuccessOf` |
643
+ | Process collections sequentially | `loop`, `iterate`, or `forEach` |
644
+ | Observe without changing outcome | `tap`, `tapError`, or `log` |
645
+ | Throw intentionally at a final edge | `unwrapOrThrow()` |
646
+ | Default intentionally at a final edge | `unwrapOr(defaultValue)` |
647
+
648
+ ## Version 3 Alpha Notes
649
+
650
+ `3.0.0-alpha.1` is a semver-major prerelease:
651
+
652
+ - `no-discard` tooling moved out of the runtime package and into dedicated packages.
653
+ - TypeScript peer support is now `>=6.0.0`.
654
+ - Deprecated APIs are not part of the current API:
655
+
656
+ - `Result#finally`
657
+ - `ResultAsync#finally`
658
+ - `ResultAsync#safeUnwrap`
659
+ - `safeTryAsync`
660
+
661
+ Use `log`, `toDisposable`, `toAsyncDisposable`, and `safeTry(async function* () { ... })` instead.
662
+ `Result#safeUnwrap()` remains available for compatibility, but new code should prefer
663
+ `yield* result`.
664
+
665
+ `tryCatch` and `tryCatchAsync` remain exported as backward-compatible aliases. Prefer `tryResult`
666
+ and `tryResultAsync` in new code and docs.
667
+
668
+ ## Documentation
669
+
670
+ This README is the project entry point. For a full guide with larger examples and the complete API
671
+ map, see [DOCUMENTATION.md](DOCUMENTATION.md).
672
+
673
+ More focused material:
674
+
675
+ - [Type-safe error handling article, English](articles/en/type-safe.md)
676
+ - [Artigo sobre tratamento de erros type-safe, Portuguese](articles/pt/type-safe.md)
677
+ - [resultar-ls package guide](packages/resultar-ls/README.md)
678
+ - [resultar-tsgo package guide](packages/resultar-tsgo/README.md)
679
+
680
+ ## Workspace
681
+
682
+ This repository is a pnpm workspace:
683
+
684
+ - `packages/resultar`: Resultar runtime package.
685
+ - `packages/resultar-ls`: TypeScript language-service diagnostics and no-discard CLI.
686
+ - `packages/resultar-tsgo`: TypeScript 7 native-preview `tsgo` wrapper plus Resultar no-discard
687
+ validation.
688
+ - `benchmarks`: benchmark package.
689
+ - `examples/language-service`: TypeScript 6 no-discard smoke example.
690
+ - `examples/tsgo`: TypeScript 7 native-preview smoke example.
691
+
692
+ Common commands:
858
693
 
859
694
  ```sh
860
695
  pnpm install
@@ -864,14 +699,10 @@ pnpm test
864
699
  pnpm run test:cov
865
700
  pnpm run build
866
701
  pnpm run check:full
702
+ pnpm run test:language-service
703
+ pnpm run bench
867
704
  ```
868
705
 
869
- Package output:
870
-
871
- - `dist/index.js`
872
- - `dist/index.js.map`
873
- - `dist/index.d.ts`
874
-
875
706
  ## License
876
707
 
877
708
  MIT