resultar 3.9.1 → 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.
package/README.md CHANGED
@@ -1,903 +1,827 @@
1
1
  # Resultar
2
2
 
3
- Typed, composable error handling for TypeScript.
3
+ **Typed, composable error handling and resilient workflows for TypeScript — from a single `Result<T, E>` to lazy, dependency-injected `ResultTask<T, E, R>`.**
4
4
 
5
- Resultar makes expected failures visible in function signatures instead of hiding them behind
6
- `throw`, rejected promises, nullable values, or undocumented conventions.
5
+ [![Node.js 24+](https://img.shields.io/badge/node-%3E%3D24-339933?logo=nodedotjs&logoColor=white)](https://nodejs.org)
6
+ [![ESM Only](https://img.shields.io/badge/module-ESM-f7df1e?logo=javascript&logoColor=black)](https://github.com/inaiat/resultar/blob/main/packages/resultar/package.json)
7
+ [![TypeScript 7 Native Checks](https://img.shields.io/badge/resultar--check-TypeScript--Go-3178c6?logo=typescript&logoColor=white)](https://github.com/inaiat/resultar/blob/main/packages/check/README.md)
8
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/inaiat/resultar/blob/main/LICENSE)
7
9
 
8
- ```ts
9
- Result<T, E>
10
- ResultAsync<T, E>
11
- ResultTask<T, E, R>
12
- ```
13
-
14
- - `T` is the success value.
15
- - `E` is the expected failure.
16
- - `R` is the typed service environment still required by a `ResultTask`.
17
- - Callers must handle the result before they can use the value.
18
-
19
- Resultar stays focused on explicit values. It does not require a framework, dependency injection
20
- container, scheduler, or application runtime.
10
+ [What's New in v4](#whats-new-in-v4) · [v4 Migration](https://github.com/inaiat/resultar/blob/main/docs/migrations/v4.md) · [Gradual Adoption](https://github.com/inaiat/resultar/blob/main/docs/gradual-adoption.md) · [Full Reference (`DOCUMENTATION.md`)](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md) · [`resultar-check` Compiler CLI/LSP](https://github.com/inaiat/resultar/blob/main/packages/check/README.md) · [Benchmarks](https://github.com/inaiat/resultar/blob/main/benchmarks/README.md) · [API Decision Guide](#api-decision-guide)
21
11
 
22
- ## Highlights
12
+ In standard TypeScript, `throw` and rejected promises are invisible in function signatures. Callers have no way of knowing what can fail—or whether they handled every failure case—without reading the implementation.
23
13
 
24
- | You need | Resultar gives you |
25
- | --- | --- |
26
- | Expected failures in the type system | `Result<T, E>` and `ResultAsync<T, E>` |
27
- | Reusable lazy workflows | `ResultTask<T, E, R>` with explicit execution boundaries and typed services |
28
- | Production errors with useful metadata | Real `Error` subclasses from `createTaggedError` |
29
- | Composable sync and async workflows | `map`, `mapErr`, `andThen`, `asyncAndThen`, and `orElse` |
30
- | Linear code without scattered `try/catch` | `safeTry` with `yield*` short-circuiting |
31
- | Explicit production async policy | Typed timeout, retry, race, concurrency, and cleanup helpers |
32
- | Exhaustive application boundaries | `matchTags` for tagged error unions |
33
- | Guardrails for ignored results | The optional native `resultar-check` CLI |
14
+ Resultar moves expected failures into the type system and gives you a complete toolkit to compose, recover, retry, time out, and exhaustively match them from synchronous domain logic to production async workflows.
34
15
 
35
- Resultar began as a fork of `neverthrow`. The current API keeps the familiar explicit Result model
36
- and adds Resultar-specific tagged errors, production async helpers, exhaustive tagged boundaries,
37
- and optional TypeScript-backed diagnostics.
16
+ ---
38
17
 
39
- ## Contents
40
-
41
- - [Install](#install)
42
- - [Why Resultar](#why-resultar)
43
- - [Quick Start](#quick-start)
44
- - [Compose Results](#compose-results)
45
- - [Tagged Errors](#tagged-errors)
46
- - [Async Work](#async-work)
47
- - [Lazy Workflows With ResultTask](#lazy-workflows-with-resulttask)
48
- - [Production Async Policies](#production-async-policies)
49
- - [Linear Workflows With Result.gen](#linear-workflows-with-resultgen)
50
- - [Recover Inside A Workflow](#recover-inside-a-workflow)
51
- - [Handle Results At Boundaries](#handle-results-at-boundaries)
52
- - [Strict Results At Production Boundaries](#strict-results-at-production-boundaries)
53
- - [Prevent Ignored Results](#prevent-ignored-results)
54
- - [HTTP Request Packages](#http-request-packages)
55
- - [API Decision Guide](#api-decision-guide)
56
- - [More Documentation](#more-documentation)
57
- - [Limitations](#limitations)
18
+ ## Why Resultar in 30 Seconds
58
19
 
59
- ## Install
20
+ ### Before: Hidden failures
60
21
 
61
- ```sh
62
- pnpm add resultar
63
- ```
64
-
65
- ```sh
66
- npm install resultar
67
- ```
68
-
69
- Requirements:
70
-
71
- - Node.js 24+
72
- - ESM only
73
- - TypeScript 7+ for the canonical `resultar-check` diagnostics workflow
74
-
75
- ```ts
76
- import { createTaggedError, ok } from 'resultar'
77
- import type { Result } from 'resultar'
78
- ```
79
-
80
- CommonJS `require('resultar')` is not exported.
81
-
82
- ## Why Resultar
83
-
84
- This signature does not tell callers that parsing can fail:
22
+ This signature promises a `number`, but silently throws at runtime:
85
23
 
86
24
  ```ts
87
25
  const parsePort = (input: string): number => {
88
- const port = Number(input)
89
-
26
+ const port = Number(input);
90
27
  if (!Number.isInteger(port) || port <= 0) {
91
- throw new Error(`Invalid port ${input}`)
28
+ throw new Error(`Invalid port ${input}`); // invisible to TypeScript!
92
29
  }
93
-
94
- return port
95
- }
30
+ return port;
31
+ };
96
32
  ```
97
33
 
98
- Resultar moves the expected failure into the contract:
34
+ ### After: Explicit, typed contracts
35
+
36
+ With Resultar, expected failures are first-class values in the return type:
99
37
 
100
38
  ```ts
101
- import { createTaggedError, ok } from 'resultar'
102
- import type { Result } from 'resultar'
39
+ import { createTaggedError, ok, type Result } from "resultar";
103
40
 
104
41
  class InvalidPortError extends createTaggedError({
105
- name: 'InvalidPortError',
106
- message: 'Invalid port $input',
42
+ name: "InvalidPortError",
43
+ message: "Invalid port $input",
107
44
  }) {}
108
45
 
109
46
  const parsePort = (input: string): Result<number, InvalidPortError> => {
110
- const port = Number(input)
111
-
112
- return Number.isInteger(port) && port > 0 ? ok(port) : InvalidPortError.err({ input })
113
- }
114
- ```
115
-
116
- The caller can now see both outcomes without reading the implementation:
117
-
118
- ```ts
119
- const port = parsePort(process.env.PORT ?? '3000')
120
-
121
- const message = port.match(
122
- (value) => `Listening on ${value}`,
123
- (error) => error.message,
124
- )
47
+ const port = Number(input);
48
+ return Number.isInteger(port) && port > 0 ? ok(port) : InvalidPortError.err({ input });
49
+ };
125
50
  ```
126
51
 
127
- Use `Err` for failures the caller can reasonably handle: invalid input, conflicts, missing records,
128
- rate limits, timeouts, and unavailable dependencies. Keep programming bugs and impossible states as
129
- normal JavaScript throws.
52
+ ### End-to-End: Compose linearly, handle exhaustively
130
53
 
131
- ## Quick Start
132
-
133
- The examples below build one account-creation workflow.
134
-
135
- Start with real tagged errors. Variables in the message template become required constructor props:
54
+ Write linear workflows without nested `try/catch` blocks. TypeScript automatically infers the combined error union across every `yield*` step and enforces exhaustive handling at your application boundary:
136
55
 
137
56
  ```ts
138
- import { createTaggedError, ok } from 'resultar'
139
- import type { Result } from 'resultar'
57
+ import { createTaggedError, ok, ResultTask, type Result } from "resultar";
140
58
 
59
+ // 1. Define real Error subclasses with typed template props ($email, $operation)
141
60
  class InvalidEmailError extends createTaggedError({
142
- name: 'InvalidEmailError',
143
- message: 'Invalid email $email',
61
+ name: "InvalidEmailError",
62
+ message: "Invalid email $email",
144
63
  }) {}
145
64
 
146
- class AccountAlreadyExistsError extends createTaggedError({
147
- name: 'AccountAlreadyExistsError',
148
- message: 'Account $email already exists',
65
+ class AccountExistsError extends createTaggedError({
66
+ name: "AccountExistsError",
67
+ message: "Account $email already exists",
149
68
  }) {}
150
69
 
151
- type Account = {
152
- readonly email: string
153
- readonly id: string
154
- }
155
- ```
156
-
157
- Return `Ok` for success and `Err` for an expected failure:
70
+ class DatabaseError extends createTaggedError({
71
+ name: "DatabaseError",
72
+ message: "Database $operation failed",
73
+ }) {}
158
74
 
159
- ```ts
160
- const normalizeEmail = (input: string): Result<string, InvalidEmailError> => {
161
- const email = input.trim().toLowerCase()
75
+ // 2. Write domain & infrastructure steps with explicit error channels
76
+ const normalizeEmail = (raw: string): Result<string, InvalidEmailError> => {
77
+ const email = raw.trim().toLowerCase();
78
+ return email.includes("@") ? ok(email) : InvalidEmailError.err({ email });
79
+ };
80
+
81
+ const registerAccount = (input: string) =>
82
+ ResultTask.gen(function* () {
83
+ const email = yield* normalizeEmail(input);
84
+
85
+ if (email === "taken@example.com") {
86
+ return yield* AccountExistsError.err({ email });
87
+ }
88
+
89
+ return yield* ResultTask.tryPromise({
90
+ try: () => db.accounts.insert({ email }),
91
+ catch: (cause) => new DatabaseError({ cause, operation: "insert-account" }),
92
+ });
93
+ });
94
+ // Inferred return type:
95
+ // ResultTask<Account, InvalidEmailError | AccountExistsError | DatabaseError>
96
+
97
+ // 3. Handle every possible error at the HTTP boundary (compile error if a tag is missed!)
98
+ const result = await registerAccount(req.body.email).runResult();
99
+ const response = result.matchTags((account) => ({ status: 201, body: account }), {
100
+ InvalidEmailError: (err) => ({ status: 400, body: { code: err._tag, message: err.message } }),
101
+ AccountExistsError: (err) => ({ status: 409, body: { code: err._tag, message: err.message } }),
102
+ DatabaseError: (err) => ({ status: 503, body: { code: err._tag, message: err.message } }),
103
+ });
104
+ ```
105
+
106
+ ---
107
+
108
+ ## Highlights & Positioning
109
+
110
+ | What you need | What Resultar provides |
111
+ | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
112
+ | **Expected failures in the type system** | `Result<T, E>` for sync values, `ResultTask<T, E, R>` for lazy async workflows, and `ResultAsync<T, E>` as the in-flight `Result` returned by `task.runResult()` |
113
+ | **Structured production errors** | Real `Error` subclasses via `createTaggedError` with `$prop` interpolation, `cause`, fingerprints, and secret redaction |
114
+ | **Linear code without `try/catch`** | `Result.gen` / `Result.fn` and `ResultTask.gen` / `ResultTask.fn` using `yield*` short-circuiting |
115
+ | **Exhaustive boundary handling** | `matchTags`, `catchTag`, `catchTags`, and general-purpose `Match` pattern matching |
116
+ | **Production async resilience** | Built-in `timeout`, `retry` (with portable `Retry.policy`), `race`, bounded `forEach`, and resource cleanup |
117
+ | **Lazy tasks & typed dependencies** | `ResultTask<T, E, R>` with typed service requirements (`R`), `AbortSignal` cancellation, and LIFO resource scopes |
118
+ | **Compiler guardrails** | Optional native [`resultar-check`](https://github.com/inaiat/resultar/blob/main/packages/check/README.md) CLI & LSP to forbid ignored results (`noDiscard`) and raw `throw` |
119
+
120
+ ### Where Resultar Sits Between `neverthrow` and `Effect`
121
+
122
+ Resultar began as a fork of `neverthrow`. It keeps the approachable `Ok` / `Err` mental model while adding structured concurrency, scoped resources, typed DI, and native compiler diagnostics—without forcing a global runtime on your codebase:
123
+
124
+ | Capability | `neverthrow` | `Resultar` | `Effect` |
125
+ | ------------------------------------------------- | --------------------------- | -------------------------------------------------------------------------- | ---------------------------------------------------- |
126
+ | **Adoption unit** | 1 function (`Result<T, E>`) | **1 function (`Result<T, E>`)** | Full `Effect<A, E, R>` ecosystem |
127
+ | **Direct `await` on async results** | Yes (`ResultAsync<T, E>`) | **Task uses explicit `task.runResult()`, returning awaitable `ResultAsync`** | No (requires `Effect.runPromise` / runtime) |
128
+ | **Tagged `Error` classes & secret redaction** | Manual | **Built-in (`createTaggedError`, `redact`)** | Built-in (`Data.TaggedError`, `Redacted`) |
129
+ | **Retries, timeouts, races & virtual clock** | Manual | **Built-in (`Retry.policy`, `timeout`, `Clock.virtual`)** | Built-in (`Schedule`, `TestClock`) |
130
+ | **Typed DI (`R`), LIFO resource scopes & fibers** | No | **Opt-in via `ResultTask<T, E, R>` & `resultar-di`** | Always present (`Effect<A, E, R>`, `Layer`, `Scope`) |
131
+ | **Compiler enforcement (`noDiscard`)** | ESLint plugin | **Native TypeScript-Go CLI & LSP (`resultar-check`)** | Language service plugin |
132
+
133
+ ---
162
134
 
163
- return email.includes('@') ? ok(email) : InvalidEmailError.err({ email })
164
- }
165
-
166
- const ensureAccountIsAvailable = (
167
- email: string,
168
- ): Result<string, AccountAlreadyExistsError> =>
169
- email === 'taken@example.com' ? AccountAlreadyExistsError.err({ email }) : ok(email)
135
+ ## Install
170
136
 
171
- const buildAccount = (email: string): Result<Account, never> =>
172
- ok({ email, id: 'acct_123' })
137
+ ```sh
138
+ pnpm add resultar
139
+ # or
140
+ npm install resultar
173
141
  ```
174
142
 
175
- Compose the steps with `andThen`. The resulting error type is inferred as a union:
143
+ - **Runtime**: Node.js 24+, ESM-only (`import { ok, Result } from 'resultar'`)
144
+ - **JSR**: Also available as [`@inaiat/resultar`](https://jsr.io/@inaiat/resultar)
145
+ - **Diagnostics (optional)**: `pnpm add -D resultar-check` (requires TypeScript 7+)
176
146
 
177
- ```ts
178
- type CreateAccountError = InvalidEmailError | AccountAlreadyExistsError
147
+ ---
179
148
 
180
- const createAccount = (input: string): Result<Account, CreateAccountError> =>
181
- normalizeEmail(input).andThen(ensureAccountIsAvailable).andThen(buildAccount)
182
- ```
149
+ ## What's New in v4
183
150
 
184
- Inspect a result directly when local branching is clearer:
151
+ - **`ResultTask<T, E, R>` is the default for new async workflows**: Lazy, repeatable execution blueprints with typed service requirements (`R`), LIFO resource cleanup (`acquireRelease`, `acquireDisposable`, `scoped`), structured child fibers (`forkChild`, `Fiber`), bounded collection concurrency (`all`, `forEach`, `validateAll`, `makeSemaphore`), portable retry policies (`Retry.policy`), and deterministic virtual time (`Clock.virtual()`).
152
+ - **Unified execution boundary (`task.runResult()` returns `ResultAsync<T, E>`)**: Execute tasks explicitly via instance or static runners (`runResult`, `runPromise`, `runExit`, `runFork`). `task.runResult()` starts the task and returns an in-flight `ResultAsync<T, E>` (`PromiseLike<Result<T, E>>`) that can be awaited directly or transformed and folded with `Result` methods (`map`, `andThen`, `catchTag`, `matchTags`, `unwrapOr`, etc.).
153
+ - **Plain-return generators (`Result.gen` / `Result.fn` & `ResultTask.gen` / `ResultTask.fn`)**: Generator workflows unwrap steps with `yield*` and return plain success values (`return value`), while legacy `safeTry` remains supported for final-`Result` generator bodies.
154
+ - **Canonical error boundaries & sound tagged recovery**: `Result.try`, `ResultAsync.try`, `ResultTask.try`, and `ResultTask.tryPromise` accept both `(fn, catchFn)` and `{ try, catch }`. Tagged recovery (`catchTag`, `catchTags`, `matchTagsPartial`) removes an error from `E` only when its handler is required and defined across all table variants.
155
+ - **Context-aware pattern matching & framework scopes**: `Match.context<Input, [ctx]>()` builds reusable route/event matchers without repeating the matched input, and [`resultar-di`](https://www.npmjs.com/package/resultar-di), [`resultar-hono`](https://www.npmjs.com/package/resultar-hono), and [`resultar-fastify`](https://www.npmjs.com/package/resultar-fastify) wire application and request scopes automatically.
156
+ - **Gradual migration & eager `ResultAsync` compatibility**: In-flight constructors (`okAsync`, `errAsync`, `unitAsync`, `fromPromise`, `fromSafePromise`, `tryResultAsync`) remain active in `resultar`, while legacy eager `ResultAsync` orchestration helpers remain supported as `@deprecated` compatibility APIs.
185
157
 
186
- ```ts
187
- const account = createAccount('person@example.com')
158
+ > Upgrading from v3? Read the full **[v4 Migration & Compatibility Guide (`docs/migrations/v4.md`)](https://github.com/inaiat/resultar/blob/main/docs/migrations/v4.md)** for the complete before/after API table, retained aliases, and `PromiseLike` boundary notes.
188
159
 
189
- if (account.isOk()) {
190
- account.value.email
191
- }
160
+ ---
192
161
 
193
- if (account.isErr()) {
194
- account.error.message
195
- }
196
- ```
162
+ ## Adopt Gradually: Three Primitives, One Mental Model
197
163
 
198
- Or consume both outcomes with `match`:
164
+ You do not need to rewrite your application to use Resultar. Start with a single function returning `Result<T, E>` and scale up only when a workflow needs async I/O or typed dependencies:
199
165
 
200
- ```ts
201
- const label = createAccount('person@example.com').match(
202
- (created) => `Created ${created.id}`,
203
- (error) => `Could not create account: ${error.message}`,
204
- )
166
+ ```mermaid
167
+ flowchart LR
168
+ R["<b>1. Result&lt;T, E&gt;</b><br/>Synchronous value (Ok / Err)<br/>Zero async overhead"]
169
+ RT["<b>2. ResultTask&lt;T, E, R&gt;</b><br/>Default for new async workflows<br/>Lazy, repeatable, explicit runners"]
170
+ RA["<b>ResultAsync&lt;T, E&gt;</b><br/>In-flight Result (PromiseLike)<br/>Await or chain like Result"]
171
+
172
+ R -->|"async I/O, retry, timeout"| RT
173
+ RT -->|"task.runResult()"| RA
174
+ RA -->|"fromResultAsync(existing or factory)"| RT
205
175
  ```
206
176
 
207
- ## Compose Results
177
+ - **`T`** is the success value.
178
+ - **`E`** is the expected failure type.
179
+ - **`R`** is the typed service environment or resource scope required before a `ResultTask` can run (`never` when no external requirements remain).
208
180
 
209
- Resultar composition keeps success and failure channels separate.
181
+ Prefer `ResultTask` for new asynchronous workflows and run it explicitly, including when
182
+ `R = never`. Providing services or closing a scope leaves a Task. Calling `task.runResult()`
183
+ (or `ResultTask.runResult(task)`) starts the task and returns `ResultAsync<T, E>` — an in-flight
184
+ `Result` (`PromiseLike<Result<T, E>>`) that can be awaited directly or transformed and folded
185
+ with the same methods as `Result`. In-flight constructors (`okAsync`, `errAsync`, `unitAsync`, `fromPromise`, `fromSafePromise`, `tryResultAsync`) are active in `resultar`, while static orchestration methods on
186
+ `ResultAsync` remain deprecated in `resultar` for existing eager contracts.
187
+ Its inbound `fromResultAsync` bridge accepts structural `PromiseLike<Result<T, E>>` values.
210
188
 
211
- ### Transform success with `map`
189
+ | Stage | When to use it | Key APIs |
190
+ | ---------------------------- | ---------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
191
+ | **1. `Result<T, E>`** | Synchronous validation, parsing, and domain rules | `ok`, `err`, `map`, `andThen`, `Result.gen`, `match` |
192
+ | **2. `ResultTask<T, E, R>`** | New asynchronous workflows; add typed services, cooperative cancellation, and scopes as needed | `ResultTask.gen`, `ResultTask.fn`, `tryPromise`, `service`, `acquireRelease`, `runResult` |
193
+ | **`ResultAsync<T, E>`** | In-flight `Result` returned by `task.runResult()` (and existing eager async contracts) | `await`, `map`, `andThen`, `catchTag`, `match`, `matchTags`, `unwrapOr`, `unwrapOrThrow` |
212
194
 
213
- ```ts
214
- const accountId = createAccount('person@example.com').map((account) => account.id)
215
- ```
195
+ ### Sync-First, Pay-As-You-Go Execution Engine
216
196
 
217
- ### Add a fallible condition with `filterOrElse`
197
+ Result values remain synchronous, and the Task runtime uses synchronous fast paths until an
198
+ asynchronous step is needed. The separate Async facade retains its memoized Promise contract:
218
199
 
219
- ```ts
220
- class UnsupportedEmailDomainError extends createTaggedError({
221
- name: 'UnsupportedEmailDomainError',
222
- message: 'Email domain $domain is not supported',
223
- }) {}
200
+ - **Explicit `yield*` interop**: Core `Result.gen` / `Result.fn` support sync and eager async generators. `ResultTask.gen` composes Results, Tasks and service tags; adapt Async with `fromResultAsync`. Satisfying `R` keeps the Task contract.
201
+ - **Zero-microtask synchronous fast path**: `Result.gen`, synchronous `ResultTask` pipelines, and `ResultTask.scoped` workflows without async steps execute inside a tight synchronous `while` loop (`runTaskLoop` / `#runGen`) without scheduling microtasks—reusing static singletons (`sharedSuccessExit`, `idleTaskIterator`, `idleRunContext`) on the happy path until a real `Promise` or cooperative `yieldEvery` checkpoint is encountered.
202
+ - **Pay only for the stage you use**: Service maps, LIFO finalizer stacks, and fiber schedulers are allocated lazily only when a workflow invokes `service()`, `acquireRelease()`, or `forkChild()`.
224
203
 
225
- const requireCompanyEmail = (
226
- email: string,
227
- ): Result<string, InvalidEmailError | UnsupportedEmailDomainError> =>
228
- normalizeEmail(email).filterOrElse(
229
- (normalized) => normalized.endsWith('@company.com'),
230
- (normalized) =>
231
- new UnsupportedEmailDomainError({ domain: normalized.split('@')[1] ?? 'unknown' }),
232
- )
233
- ```
204
+ See the [Gradual Adoption Guide](https://github.com/inaiat/resultar/blob/main/docs/gradual-adoption.md) for a step-by-step walkthrough and the [Benchmarks Guide](https://github.com/inaiat/resultar/blob/main/benchmarks/README.md) for the architectural and runtime profile.
234
205
 
235
- ### Continue with fallible work using `andThen`
206
+ ---
236
207
 
237
- ```ts
238
- const createCompanyAccount = (input: string) =>
239
- requireCompanyEmail(input).andThen(ensureAccountIsAvailable).andThen(buildAccount)
240
- ```
208
+ ## Contents
241
209
 
242
- ### Transform a failure with `mapErr`
210
+ - [What's New in v4](#whats-new-in-v4)
211
+ - [Tagged Errors (`createTaggedError`)](#tagged-errors)
212
+ - [Synchronous Workflows (`Result`)](#synchronous-workflows)
213
+ - [Linear Workflows (Result and Task generators)](#linear-workflows-with-resultgen)
214
+ - [Recovering & Handling Boundaries](#recover-and-handle-results-at-boundaries)
215
+ - [Lazy Workflows (`ResultTask`)](#lazy-workflows-with-resulttask)
216
+ - [Parameterized tasks and tagged recovery](#parameterized-tasks-and-tagged-recovery)
217
+ - [Resource scopes and application lifecycle](#resource-scopes-and-application-lifecycle)
218
+ - [Task concurrency and resilience](#task-concurrency-and-resilience)
219
+ - [Portable retry policies](#portable-retry-policies)
220
+ - [Inspect failure causes](#inspect-failure-causes)
221
+ - [In-Flight `ResultAsync` & Deprecated Eager Compatibility](#in-flight-resultasync--deprecated-eager-compatibility)
222
+ - [Pattern Matching (`Match`)](#pattern-matching-with-match)
223
+ - [Prevent Ignored Results (`resultar-check`)](#prevent-ignored-results)
224
+ - [Ecosystem Packages (HTTP, Validation, DI)](#ecosystem-packages)
225
+ - [API Decision Guide](#api-decision-guide)
226
+ - [More Documentation & Limitations](#more-documentation)
243
227
 
244
- ```ts
245
- class AccountServiceError extends createTaggedError({
246
- name: 'AccountServiceError',
247
- message: 'Account service failed for $email',
248
- }) {}
228
+ ---
249
229
 
250
- const account = createAccount(input).mapErr(
251
- (cause) => new AccountServiceError({ cause, email: input }),
252
- )
253
- ```
230
+ ## Tagged Errors
254
231
 
255
- ### Recover with `orElse`
232
+ `createTaggedError` builds real `Error` subclasses with a stable `_tag` discriminant, automatic `$variable` message interpolation, and structured serialization:
256
233
 
257
234
  ```ts
258
- const account = findAccountByEmail(email).orElse(() => createAccount(email))
259
- ```
260
-
261
- Use `pipe` when a result transform should be named and reused:
235
+ import { createTaggedError } from "resultar";
262
236
 
263
- ```ts
264
- import type { Result } from 'resultar'
237
+ class AccountAlreadyExistsError extends createTaggedError({
238
+ name: "AccountAlreadyExistsError",
239
+ message: "Account $email already exists",
240
+ }) {}
265
241
 
266
- const auditAccount = <E>(result: Result<Account, E>): Result<Account, E> =>
267
- result.tap((account) => logger.info({ accountId: account.id }, 'account ready'))
242
+ const error = new AccountAlreadyExistsError({ email: "ada@example.com" });
268
243
 
269
- const accountEmail = createAccount(input)
270
- .pipe(auditAccount)
271
- .map((account) => account.email)
244
+ error instanceof Error; // true
245
+ error instanceof AccountAlreadyExistsError; // true
246
+ error._tag; // 'AccountAlreadyExistsError'
247
+ error.message; // 'Account ada@example.com already exists'
248
+ error.email; // 'ada@example.com'
249
+ error.fingerprint; // 'AccountAlreadyExistsError:Account $email already exists'
250
+ error.toJSON(); // cycle-safe serializable object
272
251
  ```
273
252
 
274
- Callbacks passed to transform methods are normal application code. Resultar does not silently catch
275
- exceptions thrown by `map`, `andThen`, or `pipe` callbacks. Wrap uncontrolled code at the edge
276
- instead.
277
-
278
- ## Tagged Errors
279
-
280
- `createTaggedError` produces real `Error` subclasses with stable tags and structured metadata:
253
+ Every tagged error class includes static helpers to construct failures directly without `new`:
281
254
 
282
255
  ```ts
283
- const error = new AccountAlreadyExistsError({ email: 'person@example.com' })
284
-
285
- error instanceof Error // true
286
- error instanceof AccountAlreadyExistsError // true
287
- error._tag // 'AccountAlreadyExistsError'
288
- error.message // 'Account person@example.com already exists'
289
- error.email // string | number
290
- error.fingerprint // stable error fingerprint
291
- error.toJSON() // serializable metadata
256
+ AccountAlreadyExistsError.err({ email }); // Err<never, AccountAlreadyExistsError>
257
+ AccountAlreadyExistsError.taskErr({ email }); // ResultTask<never, AccountAlreadyExistsError>
258
+ AccountAlreadyExistsError.errAsync({ email }); // deprecated eager ResultAsync<never, AccountAlreadyExistsError>
259
+ AccountAlreadyExistsError.is(unknownValue); // nominal type guard
292
260
  ```
293
261
 
294
- Tagged errors support:
295
-
296
- - `_tag` for exhaustive handling;
297
- - typed message-template props;
298
- - `cause` and stack traces;
299
- - `fingerprint` and `messageTemplate`;
300
- - cycle-safe `.toJSON()` output;
301
- - `.findCause(ErrorClass)` for nested causes;
302
- - static `.is(value)` for nominal checks;
303
- - static `.err(props)` for returning an `Err` directly.
262
+ ### Refining metadata types with `.withProps()`
304
263
 
305
- Preserve the original failure as `cause` when translating infrastructure errors:
264
+ Template variables default to `string | number | Redacted<unknown>`. Chain `.withProps<...>()` to refine template property types or require additional structured metadata:
306
265
 
307
266
  ```ts
308
- class DatabaseError extends createTaggedError({
309
- name: 'DatabaseError',
310
- message: 'Database $operation failed',
311
- }) {}
267
+ class UserNotFoundError extends createTaggedError({
268
+ name: "UserNotFoundError",
269
+ message: "User $id was not found",
270
+ }).withProps<{ id: string; details?: { source: string } }>() {}
312
271
 
313
- const databaseError = new DatabaseError({
314
- cause: new Error('connection refused'),
315
- operation: 'insert-account',
316
- })
317
-
318
- databaseError.findCause(Error)
272
+ const missing = new UserNotFoundError({ id: "user_1", details: { source: "replica" } });
319
273
  ```
320
274
 
321
- Use `redact` when metadata must remain available to authorized code without leaking through error
322
- messages or JSON:
275
+ ### Preserving causes and redacting secrets
276
+
277
+ Pass `cause` to preserve the underlying exception chain and inspect it with `.findCause(ErrorClass)`. Wrap sensitive fields in `redact(value, label)` so secrets never leak into `.message` or `.toJSON()` logs:
323
278
 
324
279
  ```ts
325
- import { createTaggedError, isRedacted, redact, revealRedacted } from 'resultar'
280
+ import { createTaggedError, isRedacted, redact, revealRedacted } from "resultar";
326
281
 
327
282
  class TokenRejectedError extends createTaggedError({
328
- name: 'TokenRejectedError',
329
- message: 'Token $token was rejected',
283
+ name: "TokenRejectedError",
284
+ message: "Token $token was rejected",
330
285
  }) {}
331
286
 
332
287
  const error = new TokenRejectedError({
333
- token: redact('secret-token', 'api-token'),
334
- })
288
+ token: redact("sk_live_secret", "api-token"),
289
+ cause: new Error("signature mismatch"),
290
+ });
335
291
 
336
- error.message // 'Token <redacted:api-token> was rejected'
292
+ error.message; // 'Token <redacted:api-token> was rejected'
337
293
 
338
294
  if (isRedacted(error.token)) {
339
- revealRedacted(error.token) // 'secret-token'
295
+ revealRedacted(error.token); // 'sk_live_secret' (explicit access for authorized code)
340
296
  }
341
297
  ```
342
298
 
343
- Use `taggedEnum` for lightweight tagged states or nested reasons that do not need to be real
344
- `Error` instances. See the
345
- [tagged enum guide](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#tagged-enums)
346
- for the complete API.
299
+ > Need lightweight tagged variants or nested error reasons without full `Error` stack traces? Use `taggedEnum` — see the [Tagged Enums guide](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#tagged-enums).
347
300
 
348
- ## Async Work
301
+ ---
349
302
 
350
- `ResultAsync<T, E>` is the async counterpart to `Result<T, E>`. It is awaitable and exposes the same
351
- composition style.
303
+ ## Synchronous Workflows
352
304
 
353
- Map rejections from external code into a documented error type:
305
+ ### Creating Results
306
+
307
+ In addition to `ok(value)` and `err(error)`, Resultar provides helpers to lift nullable values, predicates, and throwing functions into `Result`:
354
308
 
355
309
  ```ts
356
- import { tryResultAsync } from 'resultar'
357
- import type { ResultAsync } from 'resultar'
310
+ import { ok, pipe, Result, tryResult } from "resultar";
358
311
 
359
- class DatabaseError extends createTaggedError({
360
- name: 'DatabaseError',
361
- message: 'Database $operation failed',
362
- }) {}
312
+ // Reject null or undefined (preserves 0, false, and "")
313
+ const envName = Result.fromNullishOr(process.env.USER_NAME, () => "missing-name" as const);
363
314
 
364
- const persistAccount = (email: string): ResultAsync<Account, DatabaseError> =>
365
- tryResultAsync(
366
- () => accountRepository.insert({ email }),
367
- (cause) => new DatabaseError({ cause, operation: 'insert-account' }),
368
- )
315
+ // Narrow or validate with a predicate (direct or curried in pipe)
316
+ const positivePort = pipe(
317
+ 3000,
318
+ Result.liftPredicate(
319
+ (n: number) => n > 0,
320
+ (n) => `invalid port: ${n}` as const,
321
+ ),
322
+ );
323
+
324
+ // Wrap synchronous code that might throw
325
+ const parsedJson = tryResult(
326
+ () => JSON.parse(rawInput) as Record<string, unknown>,
327
+ (cause) => new InvalidJsonError({ cause }),
328
+ );
369
329
  ```
370
330
 
371
- Continue from synchronous validation into async work with `asyncAndThen`:
331
+ ### Chaining and transforming
332
+
333
+ Success (`Ok`) and failure (`Err`) travel on separate channels:
372
334
 
373
335
  ```ts
374
- type CreateAccountAsyncError =
375
- | InvalidEmailError
376
- | AccountAlreadyExistsError
377
- | DatabaseError
378
-
379
- const createAccountAsync = (
380
- input: string,
381
- ): ResultAsync<Account, CreateAccountAsyncError> =>
382
- normalizeEmail(input).andThen(ensureAccountIsAvailable).asyncAndThen(persistAccount)
336
+ const accountEmail = normalizeEmail(input)
337
+ .filterOrElse(
338
+ (email) => email.endsWith("@company.com"),
339
+ (email) => new UnsupportedDomainError({ domain: email.split("@")[1] ?? "unknown" }),
340
+ )
341
+ .andThen(ensureAccountIsAvailable) // runs only on Ok; merges error unions
342
+ .map((email) => ({ id: "acct_1", email })) // transforms Ok value
343
+ .inspect((acct) => logger.info(acct.id)) // best-effort side effect on Ok
344
+ .mapErr((cause) => new AccountError({ cause })); // transforms Err value
383
345
  ```
384
346
 
385
- Once a workflow is already asynchronous, `andThen` accepts callbacks that return either `Result` or
386
- `ResultAsync`:
347
+ ### Functional pipelines (`pipe`, `flow`, `identity`, `constant`)
348
+
349
+ Result and Task transformations support fluent methods and curried functions inside `pipe`
350
+ and `flow`. `ResultAsync` also retains its existing functional forms:
387
351
 
388
352
  ```ts
389
- const provisioned = createAccountAsync(input)
390
- .andThen(assignDefaultPlan)
391
- .andThen(sendWelcomeEmail)
392
- .map((account) => ({ account, status: 'ready' as const }))
393
- ```
353
+ import { constant, flow, identity, pipe, Result } from "resultar";
394
354
 
395
- Choose the wrapper that matches the external boundary:
355
+ const sanitize = flow(
356
+ (input: string) => input.trim(),
357
+ (input) => input.toLowerCase(),
358
+ );
396
359
 
397
- | External work | Use |
398
- | --- | --- |
399
- | Run throwing synchronous code now | `tryResult(fn, toError)` |
400
- | Wrap a throwing synchronous function | `fromThrowable(fn, toError)` |
401
- | Adapt an existing promise | `fromPromise(promise, toError)` |
402
- | Run a promise or async factory | `tryResultAsync(factory, toError)` |
403
- | Wrap an async function | `fromThrowableAsync(fn, toError)` |
404
- | Adapt a callback or subscription | `ResultAsync.fromCallback(options)` |
360
+ const label = pipe(
361
+ " ADA@COMPANY.COM ",
362
+ sanitize,
363
+ normalizeEmail,
364
+ Result.map((email) => `User: ${email}`),
365
+ Result.match({ ok: identity, err: constant("Invalid user") }),
366
+ );
367
+ ```
405
368
 
406
- Prefer a factory with `tryResultAsync` when creating the promise can also throw synchronously.
369
+ ---
407
370
 
408
- ## Lazy Workflows With ResultTask
371
+ <a id="linear-workflows-with-resultgen"></a>
409
372
 
410
- `ResultTask<T, E, R>` is the lazy workflow primitive in Resultar. Creating one does not start the
411
- work; `runResult`, `runExit`, or `runPromise` executes it explicitly. `R` records required service
412
- tags and resource scope requirements. Run boundaries supply the root scope automatically and
413
- require an explicit environment for any remaining service tags.
414
-
415
- Unlike `ResultAsync`, a `ResultTask` is a reusable description of work rather than an already-started
416
- operation. Mapping, chaining, recovery, service provision, and generator composition all remain lazy.
417
-
418
- | Need | API |
419
- | --- | --- |
420
- | Create an immediate success or failure | `succeed`, `fail`, `fromResult` |
421
- | Defer synchronous work | `sync`, `try` |
422
- | Defer promise-producing work | `tryPromise` |
423
- | Transform, observe, or chain | `map`, `mapError`, `flatMap`, `andThen`, `tap`, `tapError`, `as`, `match` |
424
- | Recover typed failures | `catchAll` |
425
- | Write a linear lazy workflow | `gen` with `yield*` |
426
- | Declare and provide dependencies | `service`, `provideService`, `provideServices`, `provideServiceResolver` |
427
- | Own resources and await cleanup | `acquireRelease`, `scoped` |
428
- | Execute at the application boundary | `runExit`, `runResult`, `runPromise` |
429
- | Bridge to started promises | `fromResultAsync`, `toResultAsync`, `ResultAsync.fromTask` |
373
+ ## Linear Workflows With Result And Task Generators
374
+
375
+ Use `Result.gen` / `Result.fn` for synchronous domain steps, and `ResultTask.gen` /
376
+ `ResultTask.fn` for new asynchronous workflows. Both use plain success returns;
377
+ `yield*` unwraps successes and short-circuits expected failures.
430
378
 
431
379
  ```ts
432
- import { ResultTask } from 'resultar'
380
+ import { Result, ResultTask } from "resultar";
433
381
 
434
- const loadUser = (id: string) =>
435
- ResultTask.tryPromise({
436
- try: (signal) => fetch(`/users/${id}`, { signal }).then((response) => response.json()),
437
- catch: (cause) => new Error(`Could not load user: ${String(cause)}`),
438
- })
382
+ const validateAccount = Result.fn(function* (input: string) {
383
+ const email = yield* normalizeEmail(input);
384
+ yield* ensureAccountIsAvailable(email);
385
+ return email;
386
+ });
387
+
388
+ // persistAccount and findAccountByEmail return ResultTask<Account, ...>.
389
+ const createAccount = ResultTask.fn(function* (input: string) {
390
+ const email = yield* validateAccount(input);
391
+ return yield* persistAccount(email);
392
+ });
393
+
394
+ const getOrCreateAccount = ResultTask.fn(
395
+ function* (input: string) {
396
+ return yield* createAccount(input);
397
+ },
398
+ ResultTask.catchTag("AccountExistsError", (error) => findAccountByEmail(String(error.email))),
399
+ );
439
400
 
440
- const task = loadUser('user_123')
441
- const result = await ResultTask.runResult(task)
401
+ const result = await getOrCreateAccount(input).runResult();
442
402
  ```
443
403
 
444
- `sync` treats a thrown value as an unexpected defect. Use `try` or `tryPromise` when the boundary is
445
- expected to throw or reject and should map that cause into `E`. `tryPromise` receives the execution
446
- `AbortSignal`, so callers can cancel cooperative work without starting it early.
404
+ - Return `value` for success; use `return yield* step()` or `return yield* MyError.err(...)` for a final fallible step.
405
+ - Task generators are synchronous `function*` bodies describing asynchronous steps. Compose those steps with `yield*`; execute the completed Task with an explicit runner.
406
+ - Core `Result.gen` / `Result.fn` and legacy `safeTry` support sync and eager async bodies; see [compatibility](#in-flight-resultasync--deprecated-eager-compatibility).
407
+ - Deprecated `safeTry` remains a separate helper returning the final Result. It is not an identity alias of modern `Result.gen`.
408
+ - Its implementation belongs to the Async compatibility layer; root imports re-export the same deprecated helper. Use `ResultTask.gen` for Task workflows; Task has no `safeTry` alias.
447
409
 
448
- Choose the execution boundary based on how much information the application needs:
410
+ ---
449
411
 
450
- | Boundary | Result |
451
- | --- | --- |
452
- | `runExit(task)` | Preserves `Success`, typed `Fail`, `Die`, `Interrupt`, and `Sequential` causes |
453
- | `runResult(task)` | Returns a single typed failure as `Err`; rejects defects, interruption, and composite causes |
454
- | `runPromise(task)` | Returns `T`; rejects every failure for integration with Promise-only APIs |
412
+ ## Recover And Handle Results At Boundaries
455
413
 
456
- The error type of `runExit` and `runResult` includes deferred release failures from the root scope.
457
- Interruption rejects with `AbortError`. A composite cause rejects with `ResultTaskCauseError`, whose
458
- `cause` preserves the complete tree; use `runExit` to inspect it without rejection.
414
+ ### Local recovery inside a workflow
415
+
416
+ Use `catchTag` or `catchTags` to recover from specific tagged errors while leaving remaining error types untouched in the union:
459
417
 
460
418
  ```ts
461
- const controller = new AbortController()
462
- const exit = await ResultTask.runExit(loadUser('user_123'), {
463
- signal: controller.signal,
464
- })
419
+ // Recover a single tag; InvalidEmailError and DatabaseError stay in the error type
420
+ const account = createAccount(input).catchTag("AccountExistsError", (error) =>
421
+ findAccountByEmail(String(error.email)),
422
+ );
465
423
 
466
- if (exit._tag === 'Failure' && exit.cause._tag === 'Die') {
467
- console.error('Unexpected defect', exit.cause.defect)
468
- }
424
+ // Or recover multiple tags at once
425
+ const accountOrDraft = createAccount(input).catchTags({
426
+ AccountExistsError: (error) => findAccountByEmail(String(error.email)),
427
+ InvalidEmailError: (error) => ok({ id: "draft", email: String(error.email) }),
428
+ });
469
429
  ```
470
430
 
471
- Instance methods and their static functional forms preserve laziness and infer combined error and
472
- environment types:
431
+ Use `orElse` when recovering from the entire error channel, or `orElseSucceed` / `unwrapOr` to fall back to a plain default value.
473
432
 
474
- ```ts
475
- const userName = loadUser('user_123')
476
- .map((user) => String(user.name))
477
- .andThen((name) => ResultTask.succeed(name.trim()))
478
- .catchAll((error) => ResultTask.succeed(`unavailable: ${error.message}`))
479
- ```
433
+ ### Exhaustive handling at application boundaries
480
434
 
481
- `catchAll` recovers a single typed failure. Defects and interruption bypass recovery. Composite
482
- causes bypass recovery as `Die(ResultTaskCauseError)` with the original tree on the error's `cause`;
483
- this prevents removed error types from escaping as typed failures. The equivalent functional forms
484
- are `ResultTask.map`, `ResultTask.flatMap`, and `ResultTask.catchAll`.
435
+ At the edge of your application (HTTP controllers, CLI commands, queue consumers), turn the `Result` into a final response:
485
436
 
486
- For promise adapters that do not need an error mapper, pass the lazy callback directly:
437
+ - **`matchTags(onOk, handlers)`**: Requires a handler for every `_tag` in `E`. If a deeper layer adds a new error type, TypeScript flags the boundary at compile time.
438
+ - **`matchTagsPartial(onOk, handlers, onUnhandled)`**: Handle specific tags and route the rest to a fallback.
439
+ - **`match(onOk, onErr)` / `match({ ok, err })`**: Handle `Ok` and `Err` directly when individual tags don't matter.
487
440
 
488
- ```ts
489
- const response = ResultTask.tryPromise((signal) => fetch(url, { signal }))
490
- // ResultTask<Response, unknown>: synchronous throws and rejections remain typed failures.
491
- ```
441
+ > On `Result`, `match`, `matchTags`, `matchTagsPartial`, and `unwrapOr` return `Out` directly. On `ResultAsync` instances (such as `task.runResult()`), they return native `Promise<Out>` terminals; their static/direct/curried forms return composable `ResultAsync<Out, never>` values. On Task, they return lazy `ResultTask<Out, never, R>` operators; use `folded.runPromise()` for `Out`, or fold directly on `task.runResult()`.
492
442
 
493
- The callback runs only when the task executes. Use `tryPromise({ try, catch })` when you need a
494
- specific error type; both forms receive the runtime signal and preserve interruption semantics.
443
+ ### Strict Error-only boundaries (`StrictResult` & `StrictResultAsync`)
495
444
 
496
- Workflows can request typed services with `yield*` and receive them at the boundary. Pass the service
497
- type first, then let the second call infer its literal identifier:
445
+ While `Result<T, E>` allows any error type, `StrictResult<T, E extends Error>` and
446
+ `StrictResultAsync<T, E extends Error>` constrain synchronous and in-flight boundaries to real
447
+ `Error` subclasses. New async workflow definitions return `ResultTask<User, UserError>` with a
448
+ concrete Error union, and `task.runResult()` returns `StrictResultAsync<User, UserError>`.
498
449
 
499
- ```ts
500
- const Database = ResultTask.service<
501
- { findUser: (id: string) => Promise<string> }
502
- >()('Database')
503
-
504
- const taskWithDatabase = ResultTask.gen(function* () {
505
- const database = yield* Database
506
- return yield* ResultTask.tryPromise({
507
- try: () => database.findUser('user_123'),
508
- catch: () => 'database-error' as const,
509
- })
510
- })
511
-
512
- const resultWithDatabase = await ResultTask.runResult(taskWithDatabase, {
513
- services: { Database: { findUser: async () => 'Ada' } },
514
- })
515
- ```
450
+ ---
451
+
452
+ ## Lazy Workflows With ResultTask
516
453
 
517
- The environment requirement is part of the task type. A missing or misspelled `Database` property is
518
- a compile-time error at `runResult`, `runExit`, or `runPromise`. Dependencies can also be bound before
519
- the final boundary:
454
+ `ResultTask<T, E, R>` is the preferred primitive for new async workflows. It is an
455
+ immutable, reusable blueprint that executes only at an explicit `run*` boundary.
456
+ Prefer `ok` / `err`, `mapErr`, `andThen`, and `orElse` in new code. Deprecated `succeed` / `fail`,
457
+ `mapError`, `flatMap`, and `catchAll` retain their original contracts, as do older retry options
458
+ and match handler objects; prefer `Retry.policy(...)` and `{ ok, err }` for new recipes.
459
+ Task has no `.then` method; `await task` leaves it unexecuted, including when `R = never`.
460
+ Prefer the instance runners in new recipes: `task.runResult()`, `task.runPromise()`,
461
+ `task.runExit()`, and `task.runFork()`. The static runners remain supported with the same behavior.
462
+
463
+ Use `ok`, `err`, `mapErr`, `andThen` and `orElse` as the Task API names. Object handlers
464
+ use `{ ok, err }`, and retries take a policy created with `Retry.policy(...)`.
520
465
 
521
466
  ```ts
522
- const database = { findUser: async () => 'Ada' }
467
+ import { ResultTask } from "resultar";
468
+
469
+ const fetchUser = (id: string) =>
470
+ ResultTask.tryPromise({
471
+ try: (signal) => fetch(`/api/users/${id}`, { signal }).then((res) => res.json()),
472
+ catch: (cause) => new UserLoadError({ cause, id }),
473
+ });
523
474
 
524
- const readyWithOne = ResultTask.provideService(taskWithDatabase, Database, database)
525
- const readyWithAll = ResultTask.provideServices(taskWithDatabase, { Database: database })
475
+ const task = fetchUser("user_123"); // Nothing has executed yet!
526
476
 
527
- await ResultTask.runResult(readyWithOne)
528
- await ResultTask.runResult(readyWithAll)
477
+ // Execute explicitly at your application boundary (returns ResultAsync<User, UserLoadError>):
478
+ const result = await task.runResult();
529
479
  ```
530
480
 
531
- The direct form `ResultTask.service<DatabaseContract, 'Database'>('Database')` remains supported.
532
- The curried form can also be yielded inline: `yield* ResultTask.service<DatabaseContract>()('Database')`.
533
- Requirements belong to `R` in `ResultTask<T, E, R>`; `Result` has no service lookup API.
534
-
535
- Every tag creation has its own symbol identity. Inline tags resolve through named environments
536
- (`provideServices` or the DI module). `provideService(token, value)` supplies both an exact token binding and a named fallback.
537
- Reuse the same token to select its exact binding when identifiers collide; a separately created
538
- same-name tag can resolve the named fallback. Exact token bindings take precedence. For class services and explicit dependency maps, see
539
- [`Service.require` and `requires` in resultar-di](../di/README.md#class-services-and-explicit-requirements).
540
-
541
- Infrastructure such as a dependency-injection adapter can resolve service tags lazily with
542
- `provideServiceResolver(task, { Logger: () => loggerTask })`. Each required identifier has a
543
- typed lazy provider. Provider errors and external requirements remain in the resulting task type;
544
- resource-scope requirements also remain visible. All three providers (`provideService`,
545
- `provideServices`, `provideServiceResolver`) support data-first and curried (`.pipe`) forms.
546
- A provider whose value is incompatible with the required service contract is rejected at compile
547
- time, including through a reused curried provider; partial providers keep the remaining
548
- requirements until they are satisfied at the boundary.
549
-
550
- Adapters can use `ResultTask.makeScope()` to own resources across executions. Run tasks with
551
- `owner.use(task)` and execute `owner.close()` after consumers finish. Failed uses roll back their
552
- partial acquisitions. `ResultTask.memoize(task)` shares pending and successful executions and
553
- allows retry after failure; its first execution supplies the environment and resource scope.
554
-
555
- `ResultTask.gen` composes tasks and services linearly. On short-circuit, generator `finally` blocks
556
- are closed and any yielded cleanup tasks or services are interpreted before execution completes:
481
+ | Instance boundary | Equivalent static boundary | Return type | Behavior |
482
+ | --------------------------- | --------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------ |
483
+ | `task.runResult(options?)` | `ResultTask.runResult(task, options?)` | `ResultAsync<T, E>` | Returns an in-flight `ResultAsync` resolving typed failures as `Err`; rejects defects, interruption, and composite causes |
484
+ | `task.runPromise(options?)` | `ResultTask.runPromise(task, options?)` | `Promise<T>` | Resolves the success value; rejects every failure |
485
+ | `task.runExit(options?)` | `ResultTask.runExit(task, options?)` | `Promise<Exit<T, E>>` | Never rejects; preserves every success or failure cause as a value |
486
+ | `task.runFork(options?)` | `ResultTask.runFork(task, options?)` | `FiberHandle<T, E>` | Starts a root fiber immediately, with interruption, observation, and cleanup ownership |
487
+
488
+ Both forms accept the same `ResultTaskRunOptions<R>`: `services`, `signal`, `clock`, `scheduler`,
489
+ and `yieldEvery`. Missing or incompatible services remain compile-time errors. If `R` includes
490
+ pending scope release errors, `runResult`, `runExit`, and `runFork` include them in their error
491
+ types (`E | ScopeError<R>` internally). Every call starts a separate execution; results are not
492
+ memoized. Because `runResult` returns `ResultAsync<T, E>`, you can either `await task.runResult()`
493
+ to inspect the resolved `Result<T, E>` or chain `.map()`, `.andThen()`, `.catchTag()`, `.match()`,
494
+ `.unwrapOr()`, and `.unwrapOrThrow()` directly on `task.runResult()`. There is no public `task.run()` runner.
557
495
 
558
496
  ```ts
559
- const program = ResultTask.gen(function* () {
560
- try {
561
- return yield* loadUser('user_123')
562
- } finally {
563
- yield* ResultTask.sync(() => logger.info('load-user finished'))
564
- }
565
- })
497
+ const task = ResultTask.sync(() => "ready");
498
+ const first = await task.runResult();
499
+ const second = await ResultTask.runResult(task); // Same contract, another execution
566
500
  ```
567
501
 
568
- Generator `finally` retains its existing replacement policy if its cleanup fails. Use resource
569
- scopes below when both the body and cleanup failure must be preserved.
502
+ ### Parameterized tasks and tagged recovery
503
+
504
+ Declare typed service requirements with `ResultTask.service<Contract>()('Tag')` and compose lazy workflows with `ResultTask.gen` or `ResultTask.fn`. If a required service is missing at an explicit runner, TypeScript reports a compile-time error. `map` is synchronous; compose asynchronous steps with `andThen` and `tryPromise`:
505
+
506
+ ```ts
507
+ import { createTaggedError, ok, ResultTask } from "resultar";
508
+
509
+ class UserNotFoundError extends createTaggedError({
510
+ name: "UserNotFoundError",
511
+ message: "User $id was not found",
512
+ }) {}
570
513
 
571
- Generators also accept `yield* result` for plain `Result` values: `Ok` unwraps its value and
572
- `Err` short-circuits into `E`.
514
+ const UsersRepo = ResultTask.service<{
515
+ findById(id: string): ResultTask<{ id: string; name: string }, UserNotFoundError>;
516
+ }>()("UsersRepo");
517
+
518
+ // ResultTask.fn wraps a generator and applies post-body operators lazily per call
519
+ const loadUserDisplayName = ResultTask.fn(
520
+ function* (id: string) {
521
+ const repo = yield* UsersRepo;
522
+ const user = yield* repo.findById(id);
523
+ return user.name;
524
+ },
525
+ ResultTask.catchTag("UserNotFoundError", (err) => ok(`Guest (${err.id})`)),
526
+ );
527
+ // Type: (id: string) => ResultTask<string, never, Service<'UsersRepo', ...>>
528
+
529
+ // Provide services at the boundary (or ahead of time via ResultTask.provideServices)
530
+ const nameResult = await loadUserDisplayName("u_42").runResult({
531
+ services: {
532
+ UsersRepo: {
533
+ findById: (id) => UserNotFoundError.taskErr({ id }),
534
+ },
535
+ },
536
+ });
537
+ ```
573
538
 
574
539
  ### Resource scopes and application lifecycle
575
540
 
541
+ Use `ResultTask.acquireRelease` (or `ResultTask.acquireDisposable` for native `Disposable` / `AsyncDisposable` objects) to tie resource cleanup to a scope. Finalizers always run in reverse acquisition order (LIFO)—even when the workflow fails, throws a defect, or is interrupted:
542
+
576
543
  ```ts
577
- const connection = ResultTask.acquireRelease({
544
+ const dbResource = ResultTask.acquireRelease({
578
545
  acquire: ResultTask.tryPromise({
579
546
  try: (signal) => connectDatabase(signal),
580
547
  catch: (cause) => new ConnectionError({ cause }),
581
548
  }),
582
- release: (database, exit) => ResultTask.tryPromise({
583
- try: () => database.close(exit),
584
- catch: (cause) => new CloseError({ cause }),
549
+ release: (db, exit) =>
550
+ ResultTask.tryPromise({
551
+ try: () => db.close(exit),
552
+ catch: (cause) => new CloseError({ cause }),
553
+ }),
554
+ });
555
+
556
+ const queryProgram = ResultTask.scoped(
557
+ ResultTask.gen(function* () {
558
+ const db = yield* dbResource;
559
+ return yield* ResultTask.tryPromise({
560
+ try: (signal) => db.query("SELECT 1", signal),
561
+ catch: (cause) => new QueryError({ cause }),
562
+ });
585
563
  }),
586
- })
564
+ );
565
+ ```
587
566
 
588
- const program = ResultTask.scoped(ResultTask.gen(function* () {
589
- const database = yield* connection
590
- return yield* ResultTask.tryPromise({
591
- try: (signal) => database.query('SELECT 1', signal),
592
- catch: (cause) => new QueryError({ cause }),
593
- })
594
- }))
567
+ `acquireRelease` tracks any release error in `R` (`ResultTaskScope<CloseError>`) so inner `orElse` calls cannot accidentally swallow a pending finalizer failure before `ResultTask.scoped` (or a `run*` boundary) closes the scope.
595
568
 
596
- const exit = await ResultTask.runExit(program)
597
- ```
569
+ ### Task concurrency and resilience
598
570
 
599
- Every run owns a root scope. `scoped` closes a child scope before the next task continues. Successful
600
- acquisitions register finalizers in LIFO order; every finalizer is awaited once, even after a body
601
- failure, defect, or interruption. Failed acquisition does not register its release. Earlier resources
602
- are still released if a later acquisition fails. Each finalizer receives the same region exit,
603
- before release failures are appended. Services provided around acquisition remain available during
604
- release, including services used only by the release callback.
605
-
606
- `acquireRelease` tracks deferred errors with `ResultTaskScope<ReleaseError>` in `R`. `scoped` removes
607
- that requirement and adds its errors to `E`; run boundaries do the same for the root scope. Service
608
- provision and `catchAll` before scope closure cannot erase pending release failures. To recover a
609
- single release failure, put `catchAll` after `scoped`.
610
-
611
- If use succeeds but release fails, the failure is returned as `Err`. If both fail, `runExit` retains
612
- them as `Sequential(useCause, releaseCause)`; subsequent release failures are appended in order.
613
- Release callbacks that throw produce defects. Resources acquired during a finalizer belong to a
614
- private cleanup scope, which is closed before the next finalizer starts.
615
-
616
- Cancellation is cooperative: tasks check the signal before starting, and promise operations receive
617
- it. Rejection after abort is classified as interruption. The runtime waits for in-flight operations
618
- to settle so a resource acquired during cancellation can still be registered and released. Cleanup
619
- uses a fresh, non-aborted signal. An operation or finalizer that never settles can therefore keep
620
- the run pending; there is no preemptive cancellation, scheduler, or `Fiber` runtime in this slice.
621
-
622
- For application composition, keep ordinary factories and existing `StrictResultAsync` use cases.
623
- Use service tags only when a program needs requirements supplied at execution. Put the **whole server
624
- lifetime** inside the resource scope: acquire database, session, then HTTP; wait for shutdown; drain
625
- HTTP, close session, then close database. Returning a server from a completed scope would return
626
- already-released resources. TypeScript does not enforce resource reference lifetimes.
627
-
628
- See the runnable [application lifecycle example](../../examples/resultar/src/application-lifecycle.ts)
629
- and its [smoke test](../../examples/resultar/scripts/lifecycle-smoke.ts). Run `pnpm run example:resultar`
630
- from the workspace root. Its factory and resource contracts return `ResultTask` with database,
631
- session, and HTTP error types. Drivers expose Promises only inside `ResultTask.tryPromise` adapters,
632
- which receive the runtime signal without threading it through every application interface.
633
- Concrete adapters must clean up partial acquisitions before returning a
634
- failure and implement request draining, SSE termination, and suitable deadlines.
635
-
636
- `ResultAsync.withResource` keeps its existing best-effort release behavior. Existing `ResultAsync`
637
- instances have already started; wrapping one does not make it lazy or automatically cancelable.
638
- Use `ResultTask.fromResultAsync` to run a `ResultAsync` inside a task boundary: pass a
639
- `(signal) => ResultAsync` factory to defer creation until execution, or an existing instance to
640
- capture an already-started execution. Use `ResultTask.toResultAsync(task)` — or the
641
- `ResultAsync.fromTask(task)` alias — to start a requirement-free task immediately on the default
642
- runtime and consume it as `ResultAsync`.
643
-
644
- ## Production Async Policies
645
-
646
- Resultar models common resilience policy in the expected error channel. These helpers use lazy tasks
647
- so retries can start fresh work and racing helpers can pass a cooperative abort signal.
648
-
649
- ### Timeout stays typed
571
+ `ResultTask` pipelines share the active `AbortSignal`, `Clock`, and resource scope across retries, timeouts, races, bounded collections, and child fibers:
650
572
 
651
573
  ```ts
652
- class AccountTimeoutError extends createTaggedError({
653
- name: 'AccountTimeoutError',
654
- message: 'Account $accountId timed out after $timeoutMs milliseconds',
655
- }) {}
574
+ import { Clock, Fiber, ResultTask, Retry } from "resultar";
656
575
 
657
- const account = ResultAsync.timeout(
658
- (signal) => loadAccount(accountId, { signal }),
659
- {
660
- timeoutMs: 1_500,
661
- onTimeout: () => new AccountTimeoutError({ accountId, timeoutMs: 1_500 }),
662
- },
663
- )
576
+ const resilientTask = fetchUser("user_123")
577
+ .retry(Retry.policy({ times: 2, delayMs: 100, backoff: 2, jittered: 0.2 }))
578
+ .timeout(2_000, () => new UserTimeoutError());
579
+
580
+ // Deterministic testing with a virtual clock (no real timers):
581
+ const clock = Clock.virtual();
582
+ const pending = resilientTask.runResult({ clock });
583
+ await clock.advanceBy(2_000);
584
+ const outcome = await pending;
664
585
  ```
665
586
 
666
- If the timer wins, the timeout is returned as `Err<AccountTimeoutError>` and the task receives an
667
- abort signal. No rejected timeout promise is introduced.
587
+ - **Collections & concurrency limits**: `ResultTask.all`, `ResultTask.forEach`, and `ResultTask.validateAll` run sequentially by default or concurrently via `{ concurrency: 8 | 'unbounded' }`. Use `ResultTask.makeSemaphore(permits)` to limit concurrency across independent tasks.
588
+ - **Structured child fibers**: `yield* ResultTask.forkChild(task)` starts background work attached to the current scope. Join or cancel it with `Fiber.join(fiber)`, `Fiber.await(fiber)`, or `Fiber.interrupt(fiber)`.
668
589
 
669
- ### Retry transient failures
590
+ ### Portable retry policies
670
591
 
671
- ```ts
672
- const account = ResultAsync.retry(
673
- (attempt, signal) => loadAccount(accountId, { attempt, signal }),
674
- {
675
- times: 2,
676
- delayMs: ({ nextAttempt }) => nextAttempt * 100,
677
- jittered: 0.2,
678
- while: (error) => error._tag === 'DatabaseBusyError',
679
- },
680
- )
681
- ```
682
-
683
- `times` is the number of retries after the first attempt. Use `retryOrElse` when retry exhaustion
684
- should continue into an explicit fallback:
592
+ Use `Retry.policy` to share an immutable retry configuration between Task workflows.
593
+ The same policy can also be passed to retained eager Async factories:
685
594
 
686
595
  ```ts
687
- const account = ResultAsync.retryOrElse(
688
- (attempt, signal) => loadAccount(accountId, { attempt, signal }),
689
- {
690
- times: 2,
691
- orElse: () => loadCachedAccount(accountId),
692
- },
693
- )
694
- ```
596
+ import { ResultTask, Retry } from "resultar";
695
597
 
696
- ### Race equivalent providers
598
+ const transientPolicy = Retry.policy({
599
+ times: 3,
600
+ delayMs: 100,
601
+ backoff: 2,
602
+ jittered: 0.2,
603
+ });
697
604
 
698
- ```ts
699
- const account = ResultAsync.race(
700
- (signal) => loadAccountFromPrimary(accountId, { signal }),
701
- (signal) => loadAccountFromReplica(accountId, { signal }),
702
- )
605
+ const taskWithRetry = fetchUser("user_123").retry(transientPolicy);
606
+ const result = await taskWithRetry.runResult();
703
607
  ```
704
608
 
705
- `race` returns the first success and keeps waiting after an early failure. Use `raceFirst` when the
706
- first completed success or failure should win, or `raceAll` for several equivalent providers.
609
+ ### Inspect failure causes
707
610
 
708
- ### Bound concurrent work
611
+ When executing a task with `task.runExit()`, failures are represented as a `Cause<E>` tree (`Fail`, `Die`, `Interrupt`, `Sequential`, or `Parallel`). Use the `Cause` namespace to inspect or format them:
709
612
 
710
613
  ```ts
711
- const imported = ResultAsync.forEach(
712
- accountInputs,
713
- (input) => createAccountAsync(input.email),
714
- { concurrency: 8, discard: true },
715
- )
716
- ```
614
+ import { Cause, ResultTask } from "resultar";
717
615
 
718
- `forEach` is sequential by default and stops scheduling new work after the first `Err`. Pass a
719
- positive concurrency number for bounded work or `"unbounded"` to start every mapped task.
616
+ const exit = await queryProgram.runExit();
720
617
 
721
- Use mapped `validateAll` when every independent item should run and all failures should be returned:
722
-
723
- ```ts
724
- const validated = ResultAsync.validateAll(
725
- accountInputs,
726
- (input) => normalizeEmail(input.email).asyncMap(async (email) => email),
727
- { concurrency: 8 },
728
- )
618
+ if (exit._tag === "Failure") {
619
+ Cause.failures(exit.cause); // E[] — all expected typed errors in order
620
+ Cause.defects(exit.cause); // unknown[] — all unexpected Die defects
621
+ Cause.pretty(exit.cause); // human-readable diagnostic trace with stacks & redaction
622
+ }
729
623
  ```
730
624
 
731
- ### Pair acquisition with cleanup
625
+ > For advanced `ResultTask` details—including `interruption: 'detach'`, custom `Scheduler` instruction budgets, `provideServiceResolver`, and `catchCause` / `sandbox`—see the [Full Resultar Guide](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#lazy-tasks-and-service-requirements).
732
626
 
733
- ```ts
734
- const account = ResultAsync.withResource({
735
- acquire: (signal) => databasePool.connect({ signal }),
736
- use: (connection, signal) => connection.insertAccount(input, { signal }),
737
- release: (connection) => connection.close(),
738
- })
739
- ```
627
+ ---
740
628
 
741
- After successful acquisition, `release` runs when `use` succeeds, returns `Err`, or rejects. For
742
- stream-shaped work, use native `AsyncIterable<Result<T, E>>`; Resultar intentionally does not add a
743
- stream runtime or scheduler.
629
+ <a id="async-work-and-resilience"></a>
630
+ <a id="deprecated-resultasync-compatibility"></a>
631
+ <a id="in-flight-resultasync--deprecated-eager-compatibility"></a>
744
632
 
745
- See the full guides for
746
- [racing and timeout](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#concurrent-racing-and-timeouts),
747
- [retry](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#retrying-async-work),
748
- [concurrency](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#async-concurrency-mapping),
749
- and [resource cleanup](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#resourceful-async-iterables).
633
+ ## In-Flight `ResultAsync` & Deprecated Eager Compatibility
750
634
 
751
- ## Linear Workflows With `Result.gen`
635
+ `ResultAsync<T, E>` (and `StrictResultAsync<T, E>`) is the official in-flight `Result`
636
+ (`PromiseLike<Result<T, E>>`), exported from `resultar`.
637
+ When you execute a `ResultTask` with `task.runResult(options?)` (or `ResultTask.runResult(task, options?)`),
638
+ it returns a `ResultAsync<T, E>` that can be awaited directly for `Result<T, E>` or transformed
639
+ and consumed with the same methods as `Result` (`map`, `mapErr`, `andThen`, `orElse`,
640
+ `orElseSucceed`, `catchTag`, `catchTags`, `catchReason`, `catchReasons`, `unwrapReason`,
641
+ `filterOrElse`, `as`, `asVoid`, `if`, `when`, `whenResult`, `unless`, `unlessResult`, `inspect`,
642
+ `inspectError`, `inspectBoth`, `log`, `match`, `matchTags`, `matchTagsPartial`, `unwrapOr`,
643
+ `unwrapOrThrow`, `toAsyncDisposable`, `[Symbol.asyncIterator]`, `pipe`).
752
644
 
753
- Use `Result.gen` when a longer chain reads better as linear code. `yield*` extracts the `Ok` value and
754
- short-circuits on the first `Err`. `safeTry` remains an exact compatibility alias.
645
+ ```sh
646
+ pnpm add resultar
647
+ ```
755
648
 
756
- ```ts
757
- import { Result } from 'resultar'
758
- import type { ResultAsync } from 'resultar'
649
+ In-flight value and Promise constructors (`ResultAsync.ok`, `ResultAsync.err`, `ResultAsync.unit`,
650
+ `ResultAsync.fromPromise`, `ResultAsync.fromSafePromise`, `ResultAsync.try`, and top-level `okAsync`,
651
+ `errAsync`, `unitAsync`, `fromPromise`, `fromSafePromise`, `tryResultAsync`) remain active for
652
+ in-flight values, mocks, and Promise boundary adaptation. Legacy eager adapters (`fromCallback`,
653
+ `fromThrowableAsync`, `tryAsync`, `tryCatchAsync`, `safeTry`, `runPromise`, `ResultAsync.tryCatch`,
654
+ `ResultAsync.fromThrowable`, `ResultAsync.fromTask`, `ResultAsync.fromCallback`) and static
655
+ orchestration helpers on `ResultAsync` (`retry`, `retryOrElse`, `race`, `raceAll`, `raceFirst`,
656
+ `raceWith`, `timeout`, `withResource`, `forEach`, `validateAll`, `loop`, `iterate`, `firstSuccessOf`,
657
+ `all`, `combine`, `combineWithAllErrors`, `zip`) remain available in `resultar` as deprecated
658
+ compatibility APIs for existing eager contracts. Prefer `ResultTask` for new asynchronous workflows
659
+ and orchestration.
759
660
 
760
- const createAccountLinear = (
761
- input: string,
762
- ): ResultAsync<Account, CreateAccountAsyncError> =>
763
- Result.gen(async function* () {
764
- const email = yield* normalizeEmail(input)
765
- yield* ensureAccountIsAvailable(email)
661
+ ### Wrapping external async boundaries (eager compatibility)
766
662
 
767
- return persistAccount(email)
768
- })
769
- ```
663
+ | External boundary | Helper |
664
+ | ------------------------------------------------------------- | ----------------------------------- |
665
+ | Wrap an async/promise factory (catches sync throws too) | `tryResultAsync(factory, toError)` |
666
+ | Adapt an already-created Promise | `fromPromise(promise, toError)` |
667
+ | Wrap an async function into a reusable `ResultAsync` function | `fromThrowableAsync(fn, toError)` |
668
+ | Adapt a callback or event subscription | `ResultAsync.fromCallback(options)` |
770
669
 
771
- The object form can map unexpected throws from inside the generator while leaving yielded `Err`
772
- values unchanged:
670
+ ### Existing eager resilience policies
671
+
672
+ Existing Async timeouts, retries, races, bounded collections and best-effort release keep
673
+ their signatures and eager behavior. Preserve them during migration; new workflows use
674
+ the Task policies above. Transformation callback defects can still reject the Async Promise:
773
675
 
774
676
  ```ts
775
- const account = Result.gen({
776
- async *try() {
777
- const email = yield* normalizeEmail(input)
778
- return persistAccount(email)
779
- },
780
- catch: (cause) => new AccountServiceError({ cause, email: input }),
781
- })
782
- ```
677
+ import { ResultAsync } from "resultar";
783
678
 
784
- Prefer `yield* result`. Compatibility helpers such as `safeUnwrap` are not needed in new workflows.
679
+ // 1. Timeout with cooperative AbortSignal
680
+ const timedAccount = ResultAsync.timeout((signal) => loadAccount(accountId, { signal }), {
681
+ timeoutMs: 1_500,
682
+ onTimeout: () => new AccountTimeoutError({ accountId, timeoutMs: 1_500 }),
683
+ });
785
684
 
786
- ## Recover Inside A Workflow
685
+ // 2. Retry transient failures with backoff, jitter, and fallback
686
+ const retriedAccount = ResultAsync.retryOrElse(
687
+ (attempt, signal) => loadAccount(accountId, { attempt, signal }),
688
+ {
689
+ times: 2,
690
+ delayMs: ({ nextAttempt }) => nextAttempt * 100,
691
+ jittered: 0.2,
692
+ while: (error) => error._tag === "DatabaseBusyError",
693
+ orElse: () => loadCachedAccount(accountId),
694
+ },
695
+ );
787
696
 
788
- Use recovery methods when a failure has a useful local alternative.
697
+ // 3. Race redundant providers (returns first Ok, aborts losers)
698
+ const fastestAccount = ResultAsync.race(
699
+ (signal) => loadFromPrimary(accountId, { signal }),
700
+ (signal) => loadFromReplica(accountId, { signal }),
701
+ );
789
702
 
790
- Recover from one tagged error with `catchTag`:
703
+ // 4. Bounded concurrency over collections
704
+ const imported = ResultAsync.forEach(
705
+ users,
706
+ (user) => provisionAccount(user.email),
707
+ { concurrency: 8 }, // or validateAll(...) to collect all errors
708
+ );
791
709
 
792
- ```ts
793
- const account = createAccountAsync(input).catchTag(
794
- 'AccountAlreadyExistsError',
795
- (error) => findAccountByEmail(String(error.email)),
796
- )
710
+ // 5. Acquire-use-release bracket
711
+ const inserted = ResultAsync.withResource({
712
+ acquire: (signal) => dbPool.connect({ signal }),
713
+ use: (conn, signal) => conn.insertAccount(input, { signal }),
714
+ release: (conn) => conn.close(),
715
+ });
797
716
  ```
798
717
 
799
- Recover several tagged errors with `catchTags`:
718
+ ---
800
719
 
801
- ```ts
802
- const account = createAccountAsync(input).catchTags({
803
- AccountAlreadyExistsError: (error) => findAccountByEmail(String(error.email)),
804
- InvalidEmailError: (error) =>
805
- ok({ email: String(error.email), id: 'draft_account' }),
806
- })
807
- ```
720
+ ### Async Result generators and bridges
808
721
 
809
- Unhandled tags remain in the error channel. Use `mapErr` when the failure should be translated and
810
- `orElse` when recovery should run another Result-producing branch.
722
+ Core `Result.gen` / `Result.fn` support sync and async bodies. Async bodies
723
+ start eagerly and return `ResultAsync`:
811
724
 
812
725
  ```ts
813
- const account = loadCachedAccount(accountId)
814
- .orElse(() => loadAccount(accountId))
815
- .mapErr((cause) => new AccountServiceError({ cause, email: input }))
726
+ import { Result, ResultTask, tryResultAsync } from "resultar";
727
+
728
+ const loadLegacyAccount = (id: string) =>
729
+ Result.gen(async function* () {
730
+ const account = yield* tryResultAsync(
731
+ () => accountClient.load(id),
732
+ (cause) => new AccountLoadError({ cause }),
733
+ );
734
+ return account;
735
+ });
736
+
737
+ // The factory creates a fresh eager operation on each Task run.
738
+ const task = ResultTask.fromResultAsync(() => loadLegacyAccount("acct_1"));
739
+ const result = await task.runResult();
740
+
741
+ // task.runResult() returns ResultAsync directly.
742
+ const inFlight = task.runResult();
743
+ const sameExecution = await inFlight;
816
744
  ```
817
745
 
818
- Use recovery inside a workflow. At an application boundary, consume the final result with `match`,
819
- `matchTags`, or `matchTagsPartial`.
746
+ Passing an existing Async to `fromResultAsync(existing)` reuses its execution on every run.
747
+ The factory form receives the active signal; pass it to cooperative clients when supported.
748
+ Provide all services and close required scopes before calling `task.runResult()`.
749
+ Static/instance `ResultTask.toResultAsync` and `ResultAsync.fromTask(task)` remain supported as
750
+ deprecated aliases of `task.runResult()`, and tagged-error `.errAsync` helpers remain supported.
751
+ Result `asyncMap` / `asyncAndThen` methods and statics remain supported.
820
752
 
821
- ## Handle Results At Boundaries
753
+ Async instance `match`, `matchTags`, `matchTagsPartial` and `unwrapOr` remain native Promise terminals.
754
+ Their static/direct/curried forms return Async values for composition. At an instance boundary,
755
+ `await inFlight.match({ ok: account => account.id, err: () => 'unavailable' })` produces the raw
756
+ handler output. Async `tap` / `tapError` remain deprecated best-effort aliases of
757
+ `inspect` / `inspectError`; Task's fallible `tap` operations have a separate contract.
822
758
 
823
- Boundary code turns a Result into an HTTP response, queue acknowledgement, CLI exit code, log entry,
824
- or UI state.
759
+ ---
825
760
 
826
- Use `matchTags` when tagged errors should be handled exhaustively:
761
+ ## Pattern Matching With Match
762
+
763
+ `Match` brings exhaustive, type-narrowing pattern matching to ordinary TypeScript unions, domain events, and HTTP route error mappers. Tag and discriminator tables compile into $O(1)$ lookup tables:
827
764
 
828
765
  ```ts
829
- const response = await createAccountAsync(input).matchTags(
830
- (account) => ({
831
- body: account,
832
- statusCode: 201,
833
- }),
834
- {
835
- AccountAlreadyExistsError: (error) => ({
836
- body: { code: error._tag, message: error.message },
837
- statusCode: 409,
838
- }),
839
- DatabaseError: (error) => ({
840
- body: { code: error._tag, message: error.message },
841
- statusCode: 503,
842
- }),
843
- InvalidEmailError: (error) => ({
844
- body: { code: error._tag, message: error.message },
845
- statusCode: 400,
846
- }),
847
- },
848
- )
849
- ```
766
+ import { Match } from "resultar";
850
767
 
851
- If a handler is missing, TypeScript reports it. If only selected errors need special handling, use
852
- `matchTagsPartial` with a fallback. Use plain `match` when the distinction between individual error
853
- tags does not matter.
768
+ type DomainEvent = { _tag: "UserCreated"; id: string } | { _tag: "UserDeleted"; count: number };
854
769
 
855
- Avoid unwrapping in normal application flows. `unwrapOr`, `unwrapOrThrow`, `_unsafeUnwrap`, and
856
- `_unsafeUnwrapErr` are best reserved for deliberate final boundaries or tests.
770
+ // Build a reusable matcher function
771
+ const describeEvent = Match.type<DomainEvent>()
772
+ .tag("UserCreated", (event) => `Created ${event.id}`)
773
+ .tag("UserDeleted", (event) => `Deleted ${event.count} records`)
774
+ .exhaustive();
857
775
 
858
- ## Strict Results At Production Boundaries
776
+ // Or match a value immediately by tag
777
+ const summary = Match.valueTags(event, {
778
+ UserCreated: (e) => e.id,
779
+ UserDeleted: (e) => String(e.count),
780
+ });
781
+ ```
859
782
 
860
- `Result<T, E>` intentionally allows any failure type. Strings, enums, and small objects can be useful
861
- inside narrow local workflows.
783
+ ### Reusable HTTP error handlers with `Match.context`
862
784
 
863
- For service, HTTP, job, queue, CLI, and integration boundaries, prefer the Error-only aliases:
785
+ When multiple Hono or Fastify routes share the same error-to-HTTP mapping, extract a context-aware matcher:
864
786
 
865
787
  ```ts
866
- import type { StrictResult, StrictResultAsync } from 'resultar'
788
+ import type { Context } from "hono";
789
+ import { Match } from "resultar";
867
790
 
868
- const validateAccount = (
869
- input: string,
870
- ): StrictResult<string, InvalidEmailError> => normalizeEmail(input)
791
+ const respondError = Match.context<
792
+ UserNotFoundError | DatabaseError,
793
+ [c: Context]
794
+ >().tagsExhaustive({
795
+ UserNotFoundError: (err, c) => c.json({ error: err.message }, 404),
796
+ DatabaseError: (_err, c) => c.json({ error: "Service unavailable" }, 503),
797
+ });
871
798
 
872
- const provisionAccount = (
873
- input: string,
874
- ): StrictResultAsync<Account, CreateAccountAsyncError> => createAccountAsync(input)
799
+ // Inside a route handler:
800
+ const result = await users.findById(id);
801
+ return result.match({
802
+ ok: (user) => c.json(user, 200),
803
+ err: (error) => respondError(error, c),
804
+ });
875
805
  ```
876
806
 
877
- `StrictResult<T, E extends Error>` and `StrictResultAsync<T, E extends Error>` are type-only aliases.
878
- They keep the same Resultar API while documenting that failures carry standard `Error` behavior:
879
- messages, causes, stack traces, and structured metadata.
807
+ | Pattern need | `Match` API |
808
+ | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
809
+ | Match literals, shapes, predicates, or built-in guards (`Match.string`, `Match.number`, `Match.defined`, `Match.instanceOf`, etc.) | `when`, `whenOr`, `whenAnd`, `not` |
810
+ | Match `_tag` unions | `tag`, `tags`, `tagsExhaustive`, `valueTags`, `typeTags` |
811
+ | Match custom discriminant fields or prefixes | `discriminator`, `discriminatorsExhaustive`, `tagStartsWith` |
812
+ | Finalize matcher | `exhaustive`, `orElse`, `result` (`Result<Matched, Remaining>`), `option` |
813
+
814
+ ---
880
815
 
881
816
  ## Prevent Ignored Results
882
817
 
883
- Result values are useful only when callers handle them. Install the native `resultar-check` CLI:
818
+ A `Result` only protects your application if callers actually handle it. Install [`resultar-check`](https://github.com/inaiat/resultar/blob/main/packages/check/README.md)—a native TypeScript-Go CLI and LSP server that runs compiler typechecks and Resultar-specific rules in a single pass:
884
819
 
885
820
  ```sh
886
821
  pnpm add -D resultar-check
887
822
  ```
888
823
 
889
- Use the CLI as the authoritative local and CI check:
890
-
891
- ```json
892
- {
893
- "scripts": {
894
- "check": "resultar-check"
895
- }
896
- }
897
- ```
898
-
899
- The CLI uses TypeScript-Go to run compiler and Resultar diagnostics over the same `tsconfig.json`.
900
- Configure its rules in the project file:
824
+ Add the rule configuration to `tsconfig.json`:
901
825
 
902
826
  ```json
903
827
  {
@@ -913,108 +837,94 @@ Configure its rules in the project file:
913
837
  }
914
838
  ```
915
839
 
916
- The `plugins` entry is configuration consumed by the native CLI and stdio LSP server; it does not
917
- install an editor extension. Run `resultar-check lsp` from an editor language-server configuration.
918
- A separate TypeScript installation is not required.
840
+ Run `resultar-check` in CI or `resultar-check lsp` in your editor to catch discarded `Result` / `ResultAsync` / `ResultTask` values, unprovided `any` task requirements, raw `throw` statements (`noThrow`), and ad-hoc `try/catch` blocks (`noTryCatch`). See the [`resultar-check` README](https://github.com/inaiat/resultar/blob/main/packages/check/README.md) for all rules and editor setup.
841
+
842
+ ---
843
+
844
+ ## Ecosystem Packages
919
845
 
920
- See the
921
- [`resultar-check` guide](https://github.com/inaiat/resultar/blob/main/packages/check/README.md)
922
- for all diagnostics, severities, modes, and ignore patterns.
846
+ Resultar's companion packages integrate directly with the core primitives:
923
847
 
924
- ## HTTP Request Packages
848
+ | Package | When to use it | Documentation |
849
+ | ------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
850
+ | [`resultar-check`](https://www.npmjs.com/package/resultar-check) | Native TypeScript-Go compiler + Resultar linter & LSP | [README](https://github.com/inaiat/resultar/blob/main/packages/check/README.md) |
851
+ | [`resultar-request`](https://www.npmjs.com/package/resultar-request) | Fetch-first JSON HTTP client returning `ResultAsync` with retries & typed errors | [README](https://github.com/inaiat/resultar/blob/main/packages/request/README.md) |
852
+ | [`resultar-request-zod`](https://www.npmjs.com/package/resultar-request-zod) | Zod schema validation adapter for `resultar-request` | [README](https://github.com/inaiat/resultar/blob/main/packages/request-zod/README.md) |
853
+ | [`resultar-request-typebox`](https://www.npmjs.com/package/resultar-request-typebox) | TypeBox schema validation adapter for `resultar-request` | [README](https://github.com/inaiat/resultar/blob/main/packages/request-typebox/README.md) |
854
+ | [`resultar-di`](https://www.npmjs.com/package/resultar-di) | Typed dependency injection, class services, and scoped resources for `ResultTask` | [README](https://github.com/inaiat/resultar/blob/main/packages/di/README.md) |
855
+ | [`resultar-hono`](https://www.npmjs.com/package/resultar-hono) | Hono integration with per-request DI scopes and graceful shutdown | [README](https://github.com/inaiat/resultar/blob/main/packages/hono/README.md) |
856
+ | [`resultar-fastify`](https://www.npmjs.com/package/resultar-fastify) | Fastify plugin with application/request DI scopes and lifecycle cleanup | [README](https://github.com/inaiat/resultar/blob/main/packages/fastify/README.md) |
925
857
 
926
- Resultar's Fetch-first request packages return `ResultAsync` values for request creation, network,
927
- HTTP status, JSON parsing, validation, and retry failures.
858
+ Deprecated eager compatibility is included directly in [`resultar`](#in-flight-resultasync--deprecated-eager-compatibility); no additional package or subpath is needed.
928
859
 
929
- | Package | Use it when | Guide |
930
- | --- | --- | --- |
931
- | [`resultar-request`](https://www.npmjs.com/package/resultar-request) | You use a custom validator or decoder | [README](https://github.com/inaiat/resultar/blob/main/packages/request/README.md) |
932
- | [`resultar-request-typebox`](https://www.npmjs.com/package/resultar-request-typebox) | Your response contract is a TypeBox schema | [README](https://github.com/inaiat/resultar/blob/main/packages/request-typebox/README.md) |
933
- | [`resultar-request-zod`](https://www.npmjs.com/package/resultar-request-zod) | Your response contract is a Zod schema or transform | [README](https://github.com/inaiat/resultar/blob/main/packages/request-zod/README.md) |
860
+ Request helpers return an in-flight `ResultAsync` (`StrictResultAsync`) that starts executing on call. Their public
861
+ execution contract is unchanged. Wrap the call in a factory to create a new request per Task run:
934
862
 
935
- The adapters delegate transport, JSON parsing, retries, and error mapping to `resultar-request` and
936
- re-export its public request types. Install only the core request package and schema adapter your
937
- service needs.
863
+ ```ts
864
+ import { ResultTask } from "resultar";
865
+ import { requestJson } from "resultar-request";
866
+
867
+ const loadAccount = (id: string) =>
868
+ ResultTask.fromResultAsync((signal) =>
869
+ requestJson({
870
+ request: () => fetch(`/api/accounts/${id}`, { signal }),
871
+ validator: (value): value is { readonly id: string } =>
872
+ typeof value === "object" &&
873
+ value !== null &&
874
+ "id" in value &&
875
+ typeof value.id === "string",
876
+ }),
877
+ );
878
+
879
+ const result = await loadAccount("acct_1").runResult();
880
+ ```
881
+
882
+ ---
938
883
 
939
884
  ## API Decision Guide
940
885
 
941
- | Need | Use |
942
- | --- | --- |
943
- | Create success or failure | `ok`, `err`, `okAsync`, `errAsync` |
944
- | Create an `Ok(undefined)` | `unit`, `unitAsync` |
945
- | Transform success | `map`, `asyncMap`, `as` |
946
- | Add a fallible condition | `filterOrElse` |
947
- | Transform failure | `mapErr` |
948
- | Continue fallible work | `andThen`, `asyncAndThen` |
949
- | Recover from failure | `orElse`, `catchTag`, `catchTags` |
950
- | Wrap throwing or rejecting code | `tryResult`, `tryResultAsync`, `fromPromise` |
951
- | Write linear Result code | `Result.gen` (`safeTry` compatibility alias) |
952
- | Describe reusable lazy work | `ResultTask.succeed`, `sync`, `try`, `tryPromise` |
953
- | Compose or recover lazy work | `ResultTask.map`, `flatMap`, `andThen`, `catchAll`, `gen` |
954
- | Require or bind typed services | `ResultTask.service`, `provideService`, `provideServices`, `provideServiceResolver` |
955
- | Execute lazy work explicitly | `ResultTask.runExit`, `runResult`, `runPromise` |
956
- | Bridge to started promises | `ResultTask.fromResultAsync`, `toResultAsync`, `ResultAsync.fromTask` |
957
- | Handle a final boundary | `match`, `matchTags`, `matchTagsPartial` |
958
- | Combine independent results | `zip`, `combine`, `combineWithAllErrors` |
959
- | Try ordered fallback candidates | `firstSuccessOf` |
960
- | Process async collections | `ResultAsync.forEach`, `ResultAsync.validateAll` |
961
- | Race concurrent tasks | `ResultAsync.race`, `raceAll`, `raceFirst` |
962
- | Apply a timeout | `ResultAsync.timeout` |
963
- | Retry transient work | `ResultAsync.retry`, `retryOrElse` |
964
- | Pair acquisition and cleanup | `ResultAsync.withResource` |
965
- | Observe without changing the result | `tap`, `tapError`, `log` |
966
- | Default deliberately at an edge | `unwrapOr` |
967
- | Throw deliberately at an edge | `unwrapOrThrow` |
968
-
969
- The
970
- [complete API map](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#api-map)
971
- covers overloads, aliases, callback semantics, collection helpers, conditional helpers, tagged
972
- reasons, disposable results, and compatibility APIs.
973
-
974
- ## Coding agents
975
-
976
- Each npm build includes `dist/llms.txt`, `dist/agent/SKILL.md`, focused references, a compiled
977
- core workflow example, and `dist/agent/versions.json`. Start from these installed files so the
978
- guide matches the package version. Companion package READMEs take precedence when their installed
979
- versions differ from the recorded companion versions. The online `main` guide tracks development.
980
-
981
- The guide distinguishes `Result.gen` from `ResultTask.gen`, immediate from lazy execution,
982
- observation from effectful `tap`, and `runResult` from the non-rejecting `runExit` boundary.
886
+ | Goal | Recommended API |
887
+ | ---------------------------------------------- | ------------------------------------------------------------------------------------------ |
888
+ | Create success or failure | Sync: `ok`, `err`, `unit`; Task: `ResultTask.ok`, `ResultTask.err` |
889
+ | Define a structured error class | `createTaggedError({ name, message })` (`.withProps<...>()` for custom field types) |
890
+ | Lift nullable values or predicates | `Result.fromNullishOr`, `Result.liftPredicate` |
891
+ | Wrap throwing sync or async code | Sync: `tryResult` / `fromThrowable`; async: `ResultTask.tryPromise` |
892
+ | Transform or chain results | `map`, `mapErr`, `andThen`, `filterOrElse`, `as`, `asVoid`; Task `map` is synchronous |
893
+ | Write linear generator workflows | `Result.gen`, `Result.fn`, `ResultTask.gen`, `ResultTask.fn` |
894
+ | Recover from specific errors | `catchTag`, `catchTags`, `orElse`, `orElseSucceed`, `unwrapOr` |
895
+ | Handle results at a boundary | `matchTags`, `matchTagsPartial`, `match` |
896
+ | Combine multiple results | Sync: `Result.all`; Task: `ResultTask.all`, `forEach`, `validateAll`, `firstSuccessOf` |
897
+ | Add async timeout, retry, or race | `task.timeout`, `task.retry`, `ResultTask.race`, `raceFirst`, `Retry.policy` |
898
+ | Build lazy, cancelable workflows | `ResultTask.tryPromise`, `sync`, `try`, `service`, `provideServices` |
899
+ | Manage resource lifecycles | `ResultTask.acquireRelease`, `acquireDisposable`, `scoped`, `ensuring` |
900
+ | Run lazy tasks at an edge | `task.runResult()`, `runExit()`, `runPromise()`, `runFork()` (static forms also supported) |
901
+ | Observe without altering the channel | `inspect`, `inspectError`, `inspectBoth`, `log` |
902
+ | Preserve an existing eager Async boundary | `tryResultAsync`, `fromPromise`, `fromThrowableAsync`, `okAsync`, `errAsync` |
903
+ | Adapt eager Async or request helpers into Task | `ResultTask.fromResultAsync(signal => load(signal))` |
904
+
905
+ See the [Complete API Map](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#api-map) for all overloads, collection helpers, and compatibility aliases.
906
+
907
+ ---
908
+
909
+ ## Coding Agents
910
+
911
+ Every npm release bundles `dist/llms.txt`, `dist/agent/SKILL.md`, focused API and service references, and a compiled workflow example inside `node_modules/resultar/dist/agent/`. Point your coding agent to those installed files so its guidance always matches the exact installed version.
912
+
913
+ ---
983
914
 
984
915
  ## More Documentation
985
916
 
986
- - [Full Resultar guide](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md)
987
- - [ResultTask core RFC](https://github.com/inaiat/resultar/blob/main/docs/rfcs/rfc-0001-result-task-core.md)
988
- - [Runnable core cookbook](https://github.com/inaiat/resultar/tree/main/examples/resultar)
989
- - [Catching and recovering errors](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#catching-and-recovering-errors)
990
- - [Safe Try](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#safe-try)
991
- - [Validation error recipes](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#validation-error-recipes)
992
- - [Coming from other error-handling styles](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#coming-from-other-styles)
993
- - [Public exports and compatibility aliases](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#public-entry-point)
994
- - [Type-safe error handling article](https://github.com/inaiat/resultar/blob/main/articles/en/type-safe.md)
995
- - [Artigo sobre tratamento de erros type-safe](https://github.com/inaiat/resultar/blob/main/articles/pt/type-safe.md)
996
-
997
- Resultar is also published on [JSR as `@inaiat/resultar`](https://jsr.io/@inaiat/resultar).
998
-
999
- ## Limitations
1000
-
1001
- ResultTask implements the RFC 0001 lazy core, generator with services, and resource scopes. The
1002
- following are explicitly out of scope for this slice:
1003
-
1004
- - No `Stream`, STM, caching, clustering, RPC, or platform layer; this is not a full Effect API port.
1005
- - No implicit global runtime: every `run*` call creates its own root scope and signal, and simple
1006
- tasks need no services or configuration.
1007
- - No fibers or structured concurrency yet: `forkChild`/`join`/`interrupt`, task `race`/`timeout`,
1008
- and uniform `all`/`forEach` over the task runtime are still Phase 3. The `Cause` model covers
1009
- `Fail`, `Die`, `Interrupt`, and `Sequential`; `Parallel` is reserved for that future work. Use the
1010
- `ResultAsync` race, timeout, retry, and concurrency helpers in the meantime — they keep their own
1011
- eager, per-helper protocols.
1012
- - No `Schedule`, schedule-based retry, or injectable clock yet (Phase 4).
1013
- - No `Layer` system in the core: service graphs with their own lifecycle belong to the optional
1014
- `resultar-di` package, and the core does not depend on it.
1015
- - `ResultAsync` stays eager and awaitable. `fromResultAsync` on an existing instance captures an
1016
- already-started execution; only the factory form defers creation.
1017
- - Validated on Node.js 24+ only; other runtimes such as Bun are unvalidated.
917
+ - [Gradual Adoption: `Result` to `ResultTask`](https://github.com/inaiat/resultar/blob/main/docs/gradual-adoption.md)
918
+ - [Full Resultar Reference (`DOCUMENTATION.md`)](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md)
919
+ - [v4 Migration & Deprecated API Notes](https://github.com/inaiat/resultar/blob/main/docs/migrations/v4.md)
920
+ - [Runnable Examples (`examples/`)](https://github.com/inaiat/resultar/tree/main/examples)
921
+ - [Type-Safe Error Handling Article (EN)](https://github.com/inaiat/resultar/blob/main/articles/en/type-safe.md) | [Artigo em Português (PT-BR)](https://github.com/inaiat/resultar/blob/main/articles/pt/type-safe.md)
922
+
923
+ ### Scope & Limitations
924
+
925
+ - **Focused scope**: Resultar is not a full Effect port—it intentionally omits `Stream`, STM, distributed clustering, and a global application runtime.
926
+ - **Cooperative cancellation**: Task timeouts and fiber interruptions abort the `AbortSignal` and await scope finalizers; underlying drivers must respect the signal. `ResultTask.tryPromise` can opt into `interruption: 'detach'`. Legacy Async signal-aware helpers remain cooperative, with best-effort resource release.
927
+ - **Deferred execution vs. already-started Promises**: Task factories defer work until an explicit `run*`. Eager async helpers and async `Result.gen` / `Result.fn` start eagerly and share one execution. Wrapping an already-created Promise cannot defer or replay it.
1018
928
 
1019
929
  ## License
1020
930