resultar 3.10.0 → 4.0.0

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.
@@ -1,4 +1,4 @@
1
- # Resultar v3.7+ API Guide
1
+ # Resultar v4 Development API Guide
2
2
 
3
3
  Use this reference after checking the version installed by the consuming project. The local package
4
4
  types and source are authoritative when they differ from this guide.
@@ -9,46 +9,70 @@ types and source are authoritative when they differ from this guide.
9
9
  - [Core Types And Constructors](#core-types-and-constructors)
10
10
  - [Composition And Recovery](#composition-and-recovery)
11
11
  - [Tagged Errors](#tagged-errors)
12
+ - [Pattern Matching](#pattern-matching)
12
13
  - [Boundary Matching](#boundary-matching)
13
14
  - [Async Boundaries](#async-boundaries)
14
- - [Async Policies](#async-policies)
15
+ - [Task Policies](#task-policies)
16
+ - [Eager Async Compatibility Policies](#eager-async-compatibility-policies)
15
17
  - [Collections And Control Flow](#collections-and-control-flow)
16
- - [`safeTry`](#safetry)
18
+ - [`Result.gen` / `safeTry`](#resultgen--safetry)
17
19
  - [ResultTask (Lazy Workflows)](#resulttask-lazy-workflows)
18
20
  - [Observation And Cleanup](#observation-and-cleanup)
19
21
 
20
22
  ## Package Baseline
21
23
 
22
- Resultar v3.7 is an ESM package targeting Node.js 24+. The repository currently uses TypeScript 7.
24
+ Resultar is an ESM package targeting Node.js 24+. The repository currently uses TypeScript 7.
23
25
  Do not infer consumer requirements from memory: inspect `package.json`, the lockfile, and exported
24
26
  types first.
25
27
 
26
28
  Prefer named imports and type-only imports:
27
29
 
28
30
  ```ts
29
- import { ResultAsync, createTaggedError, ok, safeTry, tryResult, tryResultAsync } from "resultar";
31
+ import {
32
+ Result,
33
+ ResultAsync,
34
+ ResultTask,
35
+ createTaggedError,
36
+ ok,
37
+ Retry,
38
+ tryResult,
39
+ tryResultAsync,
40
+ } from "resultar";
30
41
  import type { StrictResult, StrictResultAsync } from "resultar";
31
42
  ```
32
43
 
33
- `tryCatch` and `tryCatchAsync` remain compatibility aliases. Prefer `tryResult` and
34
- `tryResultAsync` in new v3.7+ code.
44
+ `try`, the default export, and `tryCatch` remain deprecated aliases of `tryResult`.
45
+ `ResultAsync` and `StrictResultAsync` are the official in-flight `Result` types (`PromiseLike<Result<T, E>>`), returned by `task.runResult()` / `ResultTask.runResult(task)`.
46
+ Active in-flight constructors (`okAsync`, `errAsync`, `unitAsync`, `fromPromise`, `fromSafePromise`, `tryResultAsync`) and deprecated eager adapters/orchestration helpers (`fromThrowableAsync`, `fromCallback`, `tryAsync`, `tryCatchAsync`, `safeTry`, `runPromise`, and static orchestration helpers on `ResultAsync`) are exported directly from `"resultar"`.
47
+ `tryAsync` / `tryCatchAsync` retain `tryResultAsync` behavior.
48
+
49
+ `Result.gen` / `Result.fn` support sync and async generators; their async form returns eager `ResultAsync`.
50
+ Deprecated `safeTry` also supports both forms, retaining its final-Result contract. No plugin installation is needed.
35
51
 
36
52
  ## Core Types And Constructors
37
53
 
38
- | Need | API |
39
- | --------------------------------------------------- | ---------------------------------------- |
40
- | Synchronous fallible value | `Result<T, E>` |
41
- | Asynchronous fallible value | `ResultAsync<T, E>` |
42
- | Error-only sync channel | `StrictResult<T, E extends Error>` |
43
- | Error-only async channel | `StrictResultAsync<T, E extends Error>` |
44
- | Successful value | `ok(value)` / `okAsync(value)` |
45
- | Successful `undefined` | `unit()` / `unitAsync()` |
46
- | Expected failure | `err(error)` / `errAsync(error)` |
47
- | Catch synchronous throws now | `tryResult(fn, toError)` |
48
- | Wrap a synchronous throwing function | `fromThrowable(fn, toError)` |
49
- | Catch promise rejection or a throwing async factory | `tryResultAsync(input, toError)` |
50
- | Wrap an async throwing function | `fromThrowableAsync(fn, toError)` |
51
- | Deliberately leave Resultar at a terminal boundary | `runSync(result)` / `runPromise(result)` |
54
+ | Need | API |
55
+ | -------------------------------------------------- | ---------------------------------------------------- |
56
+ | Synchronous fallible value | `Result<T, E>` |
57
+ | New asynchronous workflow | `ResultTask<T, E, R>` |
58
+ | In-flight / eager async value | `ResultAsync<T, E>` |
59
+ | Error-only sync channel | `StrictResult<T, E extends Error>` |
60
+ | Error-only in-flight / eager async channel | `StrictResultAsync<T, E extends Error>` |
61
+ | Successful synchronous / lazy value | `ok(value)` / `ResultTask.ok(value)` |
62
+ | Successful synchronous / lazy `undefined` | `unit()` / `ResultTask.ok(undefined)` |
63
+ | Expected synchronous / lazy failure | `err(error)` / `ResultTask.err(error)` |
64
+ | In-flight / eager constructors | `okAsync(value)` / `unitAsync()` / `errAsync(error)` |
65
+ | Reject a missing value | `Result.fromNullishOr(value, onNullish)` |
66
+ | Construct from a predicate or refinement | `Result.liftPredicate(value, predicate, onFalse)` |
67
+ | Catch synchronous throws now | `tryResult(fn, toError)` |
68
+ | Wrap a synchronous throwing function | `fromThrowable(fn, toError)` |
69
+ | Defer a Promise factory and catch rejection | `ResultTask.tryPromise({ try, catch })` |
70
+ | Wrap an in-flight Promise or catch eagerly | `fromPromise(p, toError)` / `tryResultAsync(input, toError)` |
71
+ | Preserve a deprecated eager async function wrapper | `fromThrowableAsync(fn, toError)` |
72
+ | Leave a sync / Task channel at a terminal boundary | `runSync(result)` / `task.runPromise()` |
73
+ | Unwrap an in-flight `ResultAsync` at a boundary | `runPromise(asyncResult)` (deprecated) |
74
+
75
+ Active in-flight constructors (`okAsync`, `unitAsync`, `errAsync`, `fromPromise`, `fromSafePromise`, `tryResultAsync`) and deprecated eager compatibility helpers (`fromThrowableAsync`, `fromCallback`, `tryAsync`, `tryCatchAsync`, `safeTry`, `runPromise`) are exported from `resultar`.
52
76
 
53
77
  Use the named options form when it improves readability:
54
78
 
@@ -59,39 +83,61 @@ const parsed = tryResult({
59
83
  });
60
84
  ```
61
85
 
62
- `runSync` returns the `Ok` value or throws the `Err`; `runPromise` resolves the `Ok` value or
63
- rejects with the `Err`. Keep both at framework, bootstrap, CLI, or test boundaries.
86
+ `runSync` returns the `Ok` value or throws the `Err`. `task.runPromise()` executes a Task and
87
+ resolves its success value or rejects on failure. `runPromise` from `resultar` is the eager
88
+ compatibility terminal. Keep runners at framework, bootstrap, CLI, or test boundaries; Tasks need an explicit
89
+ runner even when they have no requirements.
90
+
91
+ `Result.fromNullishOr` preserves `NonNullable<T>` and accepts other falsy values.
92
+ `Result.liftPredicate` preserves the input type for boolean predicates and narrows it for
93
+ refinements. Both also accept curried callbacks for `pipe`; curried forms retain concrete
94
+ input types, including literals. Error factories receive the original input and run only
95
+ on rejection. Callback exceptions propagate; use `tryResult` to capture external throws.
64
96
 
65
97
  ## Composition And Recovery
66
98
 
67
- - `map` transforms an `Ok` value with an infallible function.
68
- - `mapErr` transforms an `Err` value.
99
+ `Result`, `ResultAsync`, and `ResultTask` expose direct and curried `map`, `mapErr`, `andThen`, `orElse`,
100
+ `orElseSucceed`, `unwrapOr`, `catchTag`, `catchTags`, `catchReason`, `catchReasons`,
101
+ `unwrapReason`, `inspect`, `inspectError`, `inspectBoth`, `match`, `matchTags`,
102
+ `matchTagsPartial`, `as`, `asVoid`, and `filterOrElse`. Use `pipe(result, Result.map(f))` or
103
+ `Result.map(result, f)` for the same fluent behavior. `ResultAsync` is the official in-flight `Result` class (`PromiseLike<Result<T, E>>`), returned by `task.runResult()` and exported from `resultar`. Result's `asyncMap` / `asyncAndThen`
104
+ methods and statics remain supported.
105
+
106
+ - `map` transforms an `Ok` value synchronously on Result and Task; Async also awaits Promise-returning callbacks.
107
+ - `mapErr` transforms an `Err` value synchronously on Result and Task; Async also awaits Promise-returning callbacks.
69
108
  - `filterOrElse` validates or narrows an `Ok` value and introduces a typed error on failure.
70
- - `andThen` chains fallible work. On `ResultAsync`, it accepts sync or async Resultar work.
71
- - `asyncAndThen` bridges a synchronous `Result` into `ResultAsync`.
109
+ - `andThen` chains fallible work. Async accepts a synchronous Result or another Async; Task accepts a synchronous Result or another Task. Use core `ResultTask.fromResultAsync` or `task.runResult()` (with deprecated aliases `task.toResultAsync()` and `ResultAsync.fromTask(task)`) to bridge models.
110
+ - `Result.asyncAndThen(result, callback)` / `.asyncAndThen` and `Result.asyncMap(result, callback)` / `.asyncMap`
111
+ bridge a synchronous Result into eager `ResultAsync`; both static helpers retain curried forms for `pipe`.
72
112
  - `orElse` recovers the complete error channel.
113
+ - `orElseSucceed` recovers the complete error channel with a plain fallback value.
73
114
  - `catchTag` recovers one tagged error.
74
115
  - `catchTags` recovers selected tagged errors; unhandled variants stay in the error union.
75
- - `pipe` applies named combinators without changing Result semantics.
116
+ - `catchReason` / `catchReasons` / `unwrapReason` recover or promote nested tagged `reason` fields.
117
+ - `pipe` applies named combinators without changing Result or ResultTask semantics.
76
118
 
77
119
  Let each step expose its narrow error type and let composition infer the union:
78
120
 
79
121
  ```ts
80
122
  type CreateUserError = InvalidEmailError | UserExistsError | SaveUserError;
81
123
 
82
- const createUser = (input: CreateUserInput): StrictResultAsync<User, CreateUserError> =>
83
- validateEmail(input.email)
84
- .asyncAndThen(ensureEmailAvailable)
124
+ const createUser = (input: CreateUserInput): ResultTask<User, CreateUserError> =>
125
+ ResultTask.fromResult(validateEmail(input.email))
126
+ .andThen(ensureEmailAvailable)
85
127
  .andThen((email) => saveUser({ ...input, email }));
86
128
  ```
87
129
 
130
+ Here the asynchronous helpers return Tasks. A deliberate transition into an eager compatibility
131
+ API uses `Result.asyncAndThen(validateEmail(email), ensureEmailAvailableAsync)` (or `.asyncAndThen(ensureEmailAvailableAsync)`).
132
+
88
133
  Do not manually cast the final error union. A surprising inferred union is usually evidence that a
89
134
  step has a broad or inaccurate signature.
90
135
 
91
136
  ## Tagged Errors
92
137
 
93
138
  `createTaggedError` creates nominal `Error` subclasses with stable `_tag`, message-template props,
94
- `cause`, `fingerprint`, `toJSON()`, and `.err()` support.
139
+ `cause`, `fingerprint`, `toJSON()`, and `.err()` / `.taskErr()` / `.errAsync()` support. New eager
140
+ recipes can use `errAsync(new DomainError(props))`, importing `errAsync` from `resultar`.
95
141
 
96
142
  ```ts
97
143
  class UserNotFoundError extends createTaggedError({
@@ -114,28 +160,74 @@ const missing = UserNotFoundError.err({ userId, source: "database" });
114
160
  - Use `redact`, `isRedacted`, and `revealRedacted` when sensitive error metadata must not serialize
115
161
  or log by default.
116
162
  - Use `findCause` or an instance's `findCause` to inspect a typed cause chain.
163
+ - `createTaggedError(options).withProps<Props>()` refines declared template fields and adds typed
164
+ optional or structured metadata. Template fields remain required. Existing factory calls keep
165
+ their inferred `string | number | Redacted<unknown>`
166
+ fields. Each typed declaration creates a fresh nominal class with the same configuration,
167
+ preserving metadata from getters and non-enumerable properties. Constructor input types are
168
+ checked by TypeScript, without runtime validation.
169
+ `cause` is passed as usual to the constructor and retains its `Error` or custom base type;
170
+ omit it from `Props`.
171
+
172
+ ## Pattern Matching
173
+
174
+ Check installed root exports before using the workspace `Match` addition. It supports typed pattern
175
+ matching with pipe composition and fluent methods:
176
+
177
+ Use inline `result.matchTags` for ordinary HTTP routes, capturing the native context or reply.
178
+ Use a contextual matcher when several routes share the same response mapping.
179
+
180
+ - `Match.type<Input>()` builds a reusable matcher; `Match.value(input)` evaluates cases eagerly.
181
+ - `Match.fn(selector)` preserves selector arguments. Handlers receive the narrowed value followed
182
+ by all original arguments; this supports reusable native Hono/Fastify HTTP mappings.
183
+ - `Match.context<Input, [context: Context]>()` passes the matched input and typed context once,
184
+ preserving optional/rest arguments in the compiled function. Fluent and pipe composition work.
185
+ - `when`, `whenOr`, `whenAnd` and `not` handle literals, predicates and nested plain-object/array
186
+ prefix patterns. Literal comparison uses `===`.
187
+ - `tag`, partial `tags`, `tagsExhaustive`, `valueTags` and `typeTags` handle tagged unions;
188
+ `discriminator`, `discriminators` and `discriminatorsExhaustive` support other property names.
189
+ - `exhaustive` proves full coverage; `orElse` handles the remaining input. `orElseAbsurd` throws on
190
+ unmatched input without proving completeness.
191
+ - `Match.result` returns native `Result<Matched, Remaining>`; unmatched input is retained in Err.
192
+ It does not consume an existing domain Result. `option` returns a plain `MatchOption` tagged value.
193
+ - `withReturnType<Output>()` constrains handler returns. Boolean predicates, `nonEmptyString`,
194
+ safe `instanceOf`, optional handlers and widened literal variables retain unproven cases.
195
+ - Exceptions and Promises retain ordinary JavaScript behavior; matching does not run tasks or
196
+ await handler Promises. Existing Result/ResultAsync boundary matching retains its semantics.
197
+
198
+ See `packages/resultar/README.md#pattern-matching-with-match` for guards and all operations.
117
199
 
118
200
  ## Boundary Matching
119
201
 
120
202
  Use branch methods inside an algorithm and matching when converting the completed result:
121
203
 
122
- - `isOk()` / `isErr()` narrow for direct control flow.
204
+ - `isOk()` / `isErr()` narrow a synchronous `Result` for direct control flow.
123
205
  - `match(onOk, onErr)` handles a simple boundary.
124
206
  - `matchTags(onOk, handlers)` exhaustively maps tagged errors.
125
207
  - `matchTagsPartial(onOk, handlers, fallback)` handles selected tags with a deliberate fallback.
126
208
  - `matchError(error, handlers)` handles an error value directly.
127
209
  - `matchErrorPartial(error, handlers, fallback)` partially handles an error value.
128
210
 
211
+ On synchronous `Result`, `match`, `matchTags`, and `matchTagsPartial` return `Out`.
212
+ On `ResultAsync` instances, they are native `Promise<Out>` terminals that await handler returns.
213
+ The static/direct/curried Async forms return composable `ResultAsync<Awaited<Out>, never>` values;
214
+ static `unwrapOr` similarly returns Async while the instance method returns a Promise.
215
+ On Task, they are lazy folds returning `ResultTask<Out, never, R>`; execute the folded
216
+ Task with `folded.runPromise()` for `Out`. `unwrapOr` follows the same value / Promise /
217
+ Task distinction. Task has no `.then` method, so `await task` does not execute it.
218
+
129
219
  Include an `Error` handler whenever an error union can contain an untagged `Error`. Do not invent
130
220
  `partialCatchTags`; `catchTags` already supports partial recovery.
131
221
 
132
222
  ## Async Boundaries
133
223
 
134
- Choose the wrapper based on ownership of the promise:
224
+ Prefer `ResultTask.tryPromise({ try, catch })` for new asynchronous workflows. Its
225
+ factory starts on each explicit task run. For in-flight or eager `ResultAsync` contracts, the
226
+ following constructors and wrappers (exported from `resultar`) share one execution across awaits:
135
227
 
136
228
  - `fromPromise(existingPromise, toError)` wraps a promise that has already been created.
137
229
  - `tryResultAsync(() => createPromise(), toError)` also catches synchronous factory throws and is
138
- preferred when the agent controls creation.
230
+ preferred when the caller controls creation of an in-flight `ResultAsync`.
139
231
  - `fromSafePromise(promise)` is only for promises guaranteed not to reject by contract.
140
232
  - `fromCallback(options)` adapts callback or subscription APIs and returns
141
233
  `ResultAsync<T, E | AbortError>`. Supply cleanup and forward cancellation where supported.
@@ -144,25 +236,64 @@ Choose the wrapper based on ownership of the promise:
144
236
  Always map `unknown` causes into a specific error near the external boundary:
145
237
 
146
238
  ```ts
147
- const loadUser = (id: string): StrictResultAsync<User, LoadUserError> =>
148
- tryResultAsync(
149
- () => client.loadUser(id),
150
- (cause) => new LoadUserError({ cause, id }),
151
- );
239
+ const loadUser = (id: string): ResultTask<User, LoadUserError> =>
240
+ ResultTask.tryPromise({
241
+ try: (signal) => client.loadUser(id, { signal }),
242
+ catch: (cause) => new LoadUserError({ cause, id }),
243
+ });
152
244
  ```
153
245
 
154
- ## Async Policies
246
+ `ResultTask.fromResultAsync((signal) => existingAdapter(id, signal))` defers creation of an eager
247
+ Async until each Task run. The bridge accepts structural `PromiseLike<Result<A, E>>` values and
248
+ signal-aware factories, including native Promises; callers need not construct a ResultAsync instance.
249
+ Passing an existing Async captures its already-started execution.
250
+ Request-package helpers remain eager Async APIs; create them inside the factory when composing
251
+ new Task work. Prefer `task.runResult()` (or `ResultTask.runResult(task)`) to start one
252
+ requirement-free Task as an in-flight `ResultAsync`. `ResultAsync.fromTask(task)` and static/instance
253
+ `ResultTask.toResultAsync` remain supported as deprecated compatibility aliases with the same execution boundary.
254
+
255
+ ## Task Policies
256
+
257
+ Prefer these lazy policies for new workflows:
155
258
 
156
- Use the built-in lazy policies instead of hand-written `Promise.race`, timers, or retry loops:
259
+ - `task.retry(Retry.policy(options))` closes each attempt's scope before retrying; use `orElse`
260
+ after retry when exhausted-failure recovery is application policy.
261
+ - `task.timeout(milliseconds, onTimeout)` interrupts its child and awaits finalizers.
262
+ - `ResultTask.race(left, right)` selects the first success and preserves all-failure causes in
263
+ `Parallel`; `raceFirst` selects the first completed outcome. Both await loser cleanup.
264
+ - `all`, `forEach`, and `validateAll` accept a bounded `concurrency` option.
265
+ - `acquireRelease` / `acquireDisposable` + `scoped` await release and preserve cleanup failures.
266
+
267
+ The run supplies a live clock and scheduling by default. Use explicit run options for controlled
268
+ time or cooperative instruction yields. Pass the signal from `tryPromise` to the underlying client;
269
+ Task cancellation cannot preempt a synchronous callback. See the [Task details](#resulttask-lazy-workflows)
270
+ for interruption and cause semantics.
271
+
272
+ ## Eager Async Compatibility Policies
273
+
274
+ While `ResultAsync` and `StrictResultAsync` are the official in-flight `Result` types, `ResultAsync` also retains the following deprecated static orchestration APIs for existing eager contracts.
275
+ Use its built-in signal-aware policies when maintaining those contracts:
157
276
 
158
277
  - `ResultAsync.retry(task, options)` retries typed failures and includes `AbortError` in the error
159
278
  channel. Use `times`, `delayMs`, `jittered`, `while`, and the provided signal deliberately.
160
279
  - `ResultAsync.retryOrElse(task, options)` applies a typed fallback after retry exhaustion. The
161
280
  exhausted task error is replaced by the fallback error channel.
281
+ - `Retry.policy(options)` supplies a reusable, frozen configuration snapshot to both
282
+ `ResultAsync.retry` / `retryOrElse` and `ResultTask.retry`. It retains no execution state.
283
+ Spread the policy to add an Async-only `signal` or `orElse`. Canonical callbacks receive
284
+ `RetryContext`: zero-based `attempt`, `nextAttempt`, and retries available before scheduling
285
+ (`retriesRemaining`). `while` runs only while budget remains; awaited `onRetry` is best-effort.
286
+ In ResultTask, abort/timeout stops waiting for either hook after attempt scope closure; external
287
+ work may continue, but late outcomes cannot schedule the observer, delay, or next attempt.
288
+ `delayMs` accepts a number or context function, `backoff` multiplies the base delay, and numeric
289
+ `jittered` varies it in both directions. Task also retains `ResultTaskRetryOptions`: its delay
290
+ callback receives `(retry, error)`, predicates receive `(error, retry)`, retry is one-based,
291
+ and boolean `jittered: true` samples between zero and the computed delay. Existing ResultAsync
292
+ options keep their separate callbacks and predicate calls at exhaustion.
162
293
  - `ResultAsync.timeout(task, options)` returns the task error or the typed `onTimeout` error.
163
- - `ResultAsync.race(left, right)` and `raceAll(tasks)` return the first settled result.
164
- - `ResultAsync.raceFirst(left, right)` returns the first `Ok`; if both fail, it returns the last
165
- observed `Err`.
294
+ - `ResultAsync.race(left, right)` and `raceAll(tasks)` return the first `Ok`, ignoring earlier `Err`
295
+ results. If every task fails, they return the last completed `Err`.
296
+ - `ResultAsync.raceFirst(left, right)` returns the first completed `Result`, whether `Ok` or `Err`.
166
297
  - `ResultAsync.raceWith(left, right, handlers)` exposes the winner and a handle to the still-running
167
298
  task for custom cooperative policy.
168
299
  - `ResultAsync.withResource({ acquire, use, release })` releases after every acquire-success path.
@@ -171,93 +302,207 @@ Use the built-in lazy policies instead of hand-written `Promise.race`, timers, o
171
302
 
172
303
  Race, timeout, retry, callback, and resource helpers rely on cooperative cancellation. Pass their
173
304
  `AbortSignal` into fetch, database, SDK, timer, or subscription work; aborting a wrapper cannot stop
174
- an underlying operation that ignores the signal.
305
+ an underlying operation that ignores the signal. ResultAsync races and timeouts abort pending
306
+ losers without waiting for their settlement or cleanup. ResultTask races and timeouts own their
307
+ children and await cleanup before completing.
175
308
 
176
309
  ## Collections And Control Flow
177
310
 
178
- Both `Result` and `ResultAsync` provide collection helpers:
311
+ `Result`, `ResultAsync`, and `ResultTask` provide collection and control-flow helpers:
179
312
 
180
- - `combine` preserves array/tuple/record shape and returns the first error.
181
- - `combineWithAllErrors` collects every error.
313
+ - `all` (and `combine` on `Result` / `ResultAsync`) preserve array/tuple/record shape and return the first error.
314
+ - `combineWithAllErrors` collects every error (`Result` / `ResultAsync`).
182
315
  - `validateAll` collects all validation errors; async mapped use supports bounded `concurrency`.
183
316
  - `zip` combines exactly two values.
184
317
  - `firstSuccessOf` tries candidates until one succeeds and otherwise returns the final error.
185
- - `forEach` stops at the first error; use `{ discard: true }` when successful values are irrelevant.
186
- - `when`, `unless`, `loop`, and `iterate` model Resultar-native control flow where they improve
187
- clarity.
318
+ - `forEach` stops at the first error. Result and Async accept `{ discard: true }`; use
319
+ `.asVoid()` on the returned Task when its successful array is irrelevant.
320
+ - `if`, `when`, `unless`, `loop`, and `iterate` model Resultar-native control flow where they improve
321
+ clarity. Result and Async also retain `whenResult` / `unlessResult`.
322
+ `when` and `unless` support direct, curried (`pipe`), and fluent calls. The conditions accept
323
+ boolean and Result conditions; Async additionally accepts Async conditions and Task accepts
324
+ Task conditions. Adapt between models explicitly.
188
325
 
189
- Prefer `ResultAsync.forEach(items, mapper, { concurrency })` for bounded first-error processing and
190
- `ResultAsync.validateAll(items, mapper, { concurrency })` for independent validation where every
191
- error matters.
326
+ Prefer `ResultTask.forEach(items, mapper, { concurrency })` for new bounded workflows and
327
+ `ResultTask.validateAll` for collecting independent validation errors. Existing eager Async
328
+ collection helpers retain their execution and concurrency contracts.
192
329
 
193
- ## `safeTry`
330
+ ## `Result.gen` / `safeTry`
194
331
 
195
- Use `safeTry` when a linear generator is clearer than a chain:
332
+ Use modern `Result.gen` for a synchronous linear Result flow:
196
333
 
197
334
  ```ts
198
- const workflow = safeTry(async function* () {
335
+ const workflow = Result.gen(function* () {
199
336
  const input = yield* validateInput(raw);
200
- const remote = yield* loadRemote(input);
201
- const saved = yield* saveRemote(remote);
337
+ const normalized = yield* normalizeInput(input);
202
338
 
203
- return ok(saved);
339
+ return normalized;
204
340
  });
205
341
  ```
206
342
 
207
- Use `yield*` for `Result` and `ResultAsync`. Wrap raw promises before yielding. Avoid raw `await`,
208
- `try/catch`, and legacy `safeUnwrap()` inside the generator.
343
+ `Result.gen` / `Result.fn` also accept async generators, returning eager `ResultAsync`. Deprecated
344
+ `safeTry` accepts both forms with final-Result returns. For new workflows with async work, prefer
345
+ `ResultTask.gen(function* () { ... })`, yielding Tasks, Results, and services.
346
+ Async `Result.gen` / `Result.fn` can yield Results and `ResultAsync` values. Wrap raw Promises before yielding; a Task generator
347
+ uses `tryPromise` or `fromResultAsync(factory)`.
348
+
349
+ Avoid raw `await` and broad `try/catch` inside these generators. Plain `return value` becomes
350
+ `Ok(value)` in modern `Result.gen`; fallible steps and early
351
+ errors short-circuit via `yield*` (`return yield* err(...)` or `return yield* step()`). Returning a
352
+ `Result`, `ResultAsync`, `Promise` (in sync generators), or `ResultTask` directly from `Result.gen`
353
+ is rejected at compile time. `safeTry` retains the deprecated synchronous and
354
+ asynchronous final-Result contract, separate from modern `Result.gen`.
355
+ `Result.safeUnwrap()` also remains callable with a
356
+ deprecation annotation; prefer `yield* result`.
357
+ An early `Err` returns without closing the suspended generator; `finally` does not run on that path.
358
+ `ResultTask.gen` closes its generator and interprets yielded cleanup before completing.
359
+ A missing return becomes `Ok(undefined)`, so annotate concrete workflow output types where required.
360
+ Native async generators await their return expressions; async `Result.gen` wraps the
361
+ completed iterator value in `Ok` without inspecting or assimilating it again.
362
+
363
+ `ResultGenerator<T, E = never>` annotates sync `Result.gen` / `Result.fn` bodies and reusable
364
+ sub-generators. `ResultTaskGenerator<A, E = never, R = never>` and `ResultTaskYieldOf<E = never, R = never>`
365
+ annotate Task generators. `ResultGenOptions` and `SafeTryOptions` describe the synchronous
366
+ object forms, and `ResultAsyncGenerator`, `AsyncResultGenOptions`, and `SafeTryAsyncOptions`
367
+ describe eager async bodies (all exported from `resultar`).
368
+ `Result.fn(body, ...operators)` wraps a synchronous or asynchronous parameterized body and applies
369
+ up to eight post-body operators receiving `(result, ...args)`.
370
+ `safeTry` retains final-Result constraints and remains distinct from modern `gen`
371
+ functions, which wrap plain success payloads without inspecting them.
372
+
373
+ ```ts
374
+ // Existing safeTry contract: the final return is already a Result.
375
+ const legacy = safeTry(function* () {
376
+ const input = yield* validateInput(raw);
377
+ return ok(input);
378
+ });
379
+ ```
209
380
 
210
381
  ## Function Utilities
211
382
 
212
383
  Root `pipe` supports zero through eight transforms; `flow` supports one through nine
213
384
  functions and preserves the first function's arguments/receiver. Both compose synchronously.
214
385
  `identity` returns the original value; `constant(value)` returns a callback capturing that value.
215
- Use `result.match({ ok: identity, error: constant(fallback) })` for fixed boundary fallbacks.
386
+ Use `result.match({ ok: identity, err: constant(fallback) })` for fixed boundary fallbacks.
216
387
 
217
388
  ## ResultTask (Lazy Workflows)
218
389
 
219
- `ResultTask<A, E, R>` is a lazy, reusable description of work: building one starts nothing, and
220
- each `run*` call executes it again. Never substitute `ResultAsync` (already-started) where
221
- laziness matters.
390
+ `ResultTask<A, E, R>` is the preferred API for new asynchronous workflows. It is a
391
+ lazy, reusable description: building one starts nothing, and each explicit `run*`
392
+ executes it again. It has no `.then` method, including when `R = never`.
393
+ `ResultAsync` is the in-flight `Result` returned by `task.runResult()` (and exported from `resultar`). For repeatable
394
+ Promise work, pass a factory to `ResultTask.tryPromise` or `fromResultAsync`; an
395
+ already-created Promise cannot be deferred or replayed.
396
+
397
+ Prefer instance runners in new recipes; static forms remain equivalent and supported:
398
+
399
+ | Instance form | Equivalent static form | Boundary contract |
400
+ | --------------------------- | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
401
+ | `task.runResult(options?)` | `ResultTask.runResult(task, options?)` | `ResultAsync<A, E \| ScopeError<R>>`; returns a single typed failure as `Err`, rejects defects, interruption, and composite causes |
402
+ | `task.runPromise(options?)` | `ResultTask.runPromise(task, options?)` | `Promise<A>`; resolves the value or rejects any failure |
403
+ | `task.runExit(options?)` | `ResultTask.runExit(task, options?)` | `Promise<Exit<A, E>>`; preserves every outcome without rejection |
404
+ | `task.runFork(options?)` | `ResultTask.runFork(task, options?)` | Starts a root fiber immediately and returns `FiberHandle<A, E>` |
405
+
406
+ Both forms accept the same `ResultTaskRunOptions<R>` and check required services. Options include
407
+ `services`, `signal`, `clock`, `scheduler`, and `yieldEvery`. Pending scope release errors join
408
+ the runner error type (`E | ScopeError<R>` internally). Each call executes again; no runner memoizes
409
+ the result, and there is no public `task.run()` method.
410
+
411
+ ```ts
412
+ const task = ResultTask.sync(() => "ready");
413
+ const first = await task.runResult();
414
+ const second = await ResultTask.runResult(task); // Same contract, separate execution
415
+ ```
222
416
 
223
- - Build: `succeed`, `fail`, `fromResult`, `sync`, `try`, `tryPromise` (`try`/`tryPromise` map
417
+ - Build: `ok`, `err`, `fromResult`, `sync`, `try`, `tryPromise` (`try`/`tryPromise` map
224
418
  external throws into `E`; `sync` treats them as defects).
225
- - Compose lazily: `map`, `flatMap`, `catchAll`, `catchTag`, partial `catchTags`, `tap`, `gen` with `yield*` (also accepts
226
- `yield* result`: `Ok` unwraps, `Err` short-circuits into `E`).
227
- - `fn(function* (args) { ... })` creates a lazy parameterized function, capturing arguments/
228
- receiver and creating fresh generators on each execution; infer all three task channels.
419
+ - Compose lazily: `map`, `as`, `asVoid`, `filterOrElse`, `andThen`, `orElse`, `orElseSucceed`, `unwrapOr`, `catchCause`, `sandbox`, `unsandbox`, `catchTag`, partial `catchTags`, `catchReason`, `catchReasons`, `unwrapReason` (`andThen`/`orElse`/`catchCause`/`catchTag`/`catchTags`/`catchReason`/`catchReasons` accept callbacks returning `ResultTask` or synchronous `Result`), `match`, `matchTags`, `matchTagsPartial`, `inspect`, `inspectError`, `inspectCause`, `inspectBoth`, `tap`, `tapError`, `tapCause`, `tapBoth`, `onExit`, `delay`, `timed`, `zip` (accepting `{ concurrent?: boolean }`), `makeSemaphore` / `Semaphore`, `firstSuccessOf`, `if`, `when`, `unless`, `loop`, `iterate`, `gen` with `yield*` (constrained at compile time via `ResultTaskYieldable` to `ResultTask`, `Result`, and `ServiceTag`; `Ok` unwraps, `Err` short-circuits into `E`).
420
+ - `map` is synchronous and preserves returned values. Compose an asynchronous step with
421
+ `andThen` and `ResultTask.tryPromise` instead of expecting `map` to await a Promise.
422
+ - `fn(function* (args) { ... }, ...operators)` creates a lazy parameterized function, capturing arguments/
423
+ receiver, applying up to eight post-body pipeline operators receiving `(task, ...args)`, and creating fresh generators on each execution; infer all three task channels.
229
424
  - Tagged task recovery has instance/direct/curried forms. Handler errors and requirements
230
425
  join the source; unhandled tags and scope markers remain. Defects/interruptions pass through;
231
- composites follow `catchAll`'s `ResultTaskCauseError` policy.
426
+ composites follow `orElse`'s `ResultTaskCauseError` policy. Use `catchCause` or `sandbox` / `unsandbox` when recovering from defects (`Die`) or composite (`Sequential` / `Parallel`) causes without swallowing external interruption or pending scope release failures.
232
427
  - `acquireDisposable` prefers async disposal, awaits it once in LIFO order, and treats
233
428
  disposal failures as defects while preserving sequential body/cleanup causes.
429
+ - `ensuring(finalizer)` and `onExit((exit) => finalizer)` run a cleanup task or callback unconditionally on `Success`, `Fail`, `Die`, and `Interrupt` with an uninterrupted signal, combining body and finalizer failures into `Cause.Sequential`.
234
430
  - Provide services: `yield* Tag`, then `provideService` / `provideServices` /
235
431
  `provideServiceResolver` (data-first and curried); missing or incompatible providers are
236
- compile-time errors, never runtime surprises.
432
+ checked at typed provider and run boundaries.
237
433
  - Resources: `acquireRelease` / `acquireDisposable` + `scoped`; LIFO awaited finalizers run on every exit; deferred
238
- release errors live in `R` until `scoped` folds them into `E`; `catchAll` recovers a release
434
+ release errors live in `R` until `scoped` folds them into `E`; `orElse` recovers a release
239
435
  failure only after `scoped`.
240
- - Boundaries: `runExit` preserves `Success`/`Fail`/`Die`/`Interrupt`/`Sequential`; `runResult`
241
- returns one `Err` and rejects the rest (`AbortError` for interrupts, `ResultTaskCauseError`
242
- for composites); `runPromise` unwraps or rejects. Cancellation is cooperative: no fibers,
243
- `race`, `timeout`, or scheduler exist on `ResultTask` yet — use the `ResultAsync` helpers.
244
-
245
- `Result.gen` is the canonical name for linear Result workflows; `safeTry` remains as its alias.
436
+ - Boundaries: `runExit` preserves `Success`/`Fail`/`Die`/`Interrupt`/`Sequential`/`Parallel`; `runResult`
437
+ returns `ResultAsync<A, E | ScopeError<R>>` (resolving one `Err` and rejecting the rest: `AbortError` for interrupts, `ResultTaskCauseError`
438
+ for composites); `runPromise` unwraps or rejects. Cancellation is cooperative: fibers belong to scopes, and race/timeout await losing child cleanup.
439
+ `all`/`forEach` accept a concurrency limit; `validateAll` collects typed failures in input order; `ResultTask.makeSemaphore(permits)` / `Semaphore.make(permits)` shares a FIFO permit pool across tasks and fibers with `withPermit` and `withPermits`.
440
+ Retry closes each attempt scope and uses the run clock for backoff and jitter.
441
+ - `tryPromise({ try, catch, interruption })` defaults to `'wait'`: an in-flight promise settles
442
+ before closure. `'detach'` stops waiting after abort without stopping the underlying work.
443
+ Late outcomes are observed but do not replace the interrupted Exit; adapters own resources
444
+ acquired later. The callback-only overload keeps `'wait'`, and registered finalizers remain awaited.
445
+ - Fibers keep the existing `join` / `interrupt` shape. Namespace `Fiber.await(fiber)` lazily
446
+ observes the full Exit without requesting interruption; `join` unwraps the result and `interrupt`
447
+ requests cancellation and waits for finalizers.
448
+ - `Fiber.join(fiber, { interruption: 'detach' })` stops waiting on waiter cancellation without
449
+ canceling the target or consuming its failure. Default `'wait'` preserves existing behavior.
450
+ The owning scope still closes its children on exit.
451
+ - `task.runFork(options)` immediately starts a root run and returns `FiberHandle<A, E>`.
452
+ Native handles from `runFork` / `forkChild` have `id`, `poll()` and `addObserver(callback)`;
453
+ namespace `Fiber.poll` / `Fiber.addObserver` delegate the same diagnostics. Poll is `undefined`
454
+ while running; observers and snapshots expose the full Exit after cleanup. Observers return an
455
+ unsubscribe function, notify once, and ignore callback failures. Diagnostics do not consume failures.
456
+ - Run options accept `yieldEvery` (default 2048; positive safe integer or `Infinity` to disable)
457
+ and `scheduler: TaskScheduler`. `Scheduler.live` uses `setImmediate`; a custom scheduler provides
458
+ `yield(signal): PromiseLike<void>`. A dispatcher throw/rejection produces a `Die` and switches that
459
+ fiber to `Scheduler.live`, still attempting release instructions even if the dispatcher failed
460
+ during cleanup; release failures remain alongside it in `Cause.Sequential`.
461
+ Nested execution shares one
462
+ fiber budget; children get independent budgets. This schedules between instructions, not within
463
+ a synchronous callback, and is independent of the run clock.
464
+ `Clock.virtual().advanceBy` drains pending default scheduler yields for that clock before advancing;
465
+ other clocks and custom scheduler queues remain independently controlled.
466
+ An infinite runnable chain that never waits for other work prevents virtual time from advancing.
467
+
468
+ Inside both `Result.gen` and `ResultTask.gen`, return a plain success value and use `yield*` for
469
+ fallible steps or early errors; `safeTry` retains its final Result contract.
470
+ Prefer Task's `ok`, `err`, `mapErr`, `andThen`, and `orElse`, with their
471
+ instance/direct/curried forms where applicable. Deprecated `succeed` / `fail`, `mapError`,
472
+ `flatMap`, and `catchAll` retain their contracts. Task object handlers support `{ ok, err }`,
473
+ `{ ok, error }`, and `{ onSuccess, onFailure }`; positional `match(onOk, onErr)` also remains available.
474
+ `task.runResult()` eagerly starts one requirement-free Task
475
+ execution and returns a memoized `ResultAsync`. `ResultAsync.fromTask(task)` and static/instance `ResultTask.toResultAsync`
476
+ remain supported as deprecated aliases of `runResult()`.
477
+ `ResultTask.fromResultAsync(existing)` accepts any `PromiseLike<Result<A, E>>` and observes that
478
+ execution; its signal-aware factory form creates fresh work on each Task run.
246
479
  For lazy `ResultTask` workflows with typed services and scopes, see `packages/resultar/README.md`
247
480
  (Lazy Workflows With ResultTask) and `docs/rfcs/rfc-0001-result-task-core.md`.
248
481
 
249
- ## Observation And Cleanup
250
-
251
- On `Result` and `ResultAsync`, `tap`, `tapError`, and `log` preserve the original result. Callback throws and rejected callback
252
- promises are intentionally ignored, so use them only for best-effort logging, metrics, tracing, and
253
- observation. Use `andThen`, `orElse`, or a Resultar boundary when side-effect failure must alter the
254
- workflow.
482
+ Use runtime `Cause.isCause`, `hasFails`, `hasDies`, `hasInterrupts`, `failures`, and `defects` to inspect the existing `Cause<E>`
483
+ tree. `Cause.findError` selects its first typed error left to right, returning
484
+ `Result<E, Cause<never>>`; when no typed failure exists, `Err` contains the original tree.
485
+ `Cause.pretty` formats every leaf in order, retaining Error stacks, circular payload handling,
486
+ and JSON redaction. Traversal is iterative; sequential and parallel structure is retained.
487
+ Use `findCause(error, ErrorClass)` for a JavaScript Error's own cause chain.
255
488
 
256
- `toDisposable` and `toAsyncDisposable` integrate with Node.js `using` / `await using`; their
257
- cleanup callbacks are also best-effort. Prefer `withResource` for acquire/use/release workflows.
489
+ ## Observation And Cleanup
258
490
 
259
- On `ResultTask`, `tap` and `tapError` are lazy effectful composition. Returned task errors and
260
- requirements join the workflow type. A thrown callback or rejected Promise becomes `Die`;
261
- a returned `ResultTask.fail(error)` becomes a typed failure. Use an explicitly recovered task
262
- when observation must be best-effort. `runExit` preserves these exits; `runResult` can reject.
263
- Do not substitute `ResultAsync.withResource` for awaited ResultTask resource ownership.
491
+ Across `Result`, `ResultAsync`, and `ResultTask`, `inspect` / `inspectError` / `inspectBoth` and
492
+ `log` preserve the original result. Task object handlers use `{ ok, err }`; Result and Async
493
+ also accept their older object forms. Callback
494
+ throws and rejected callback Promises are ignored, and returned tasks are not executed
495
+ or composed. Deprecated `Result` and `ResultAsync` `tap` / `tapError` retain that
496
+ best-effort observation behavior.
497
+
498
+ Result `toDisposable` and Async `toAsyncDisposable` integrate with Node.js `using` / `await using`; their
499
+ cleanup callbacks are also best-effort. `ResultAsync.withResource` preserves its existing
500
+ best-effort release contract. Task has no `toAsyncDisposable`; use scoped acquisition or `ensuring`.
501
+
502
+ On `ResultTask`, `tap` / `tapError` / `tapCause` / `tapBoth` provide lazy effectful composition. Returned task errors and
503
+ requirements join the workflow type. Callback throws and rejected Promises become `Die`;
504
+ a returned `ResultTask.err(error)` or `err(error)` becomes a typed failure. `inspect` / `inspectError` / `inspectCause` /
505
+ `inspectBoth` (`{ ok, err }`) are the best-effort
506
+ alternative: they ignore observer failures and never execute returned tasks. `runExit` preserves
507
+ every exit; `runResult` can reject. Do not substitute best-effort `withResource` release for
508
+ awaited `acquireRelease` + `scoped` resource ownership.