resultar 3.10.0 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,961 +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
354
306
 
355
- ```ts
356
- import { tryResultAsync } from 'resultar'
357
- import type { ResultAsync } from 'resultar'
307
+ In addition to `ok(value)` and `err(error)`, Resultar provides helpers to lift nullable values, predicates, and throwing functions into `Result`:
358
308
 
359
- class DatabaseError extends createTaggedError({
360
- name: 'DatabaseError',
361
- message: 'Database $operation failed',
362
- }) {}
309
+ ```ts
310
+ import { ok, pipe, Result, tryResult } from "resultar";
363
311
 
364
- const persistAccount = (email: string): ResultAsync<Account, DatabaseError> =>
365
- tryResultAsync(
366
- () => accountRepository.insert({ email }),
367
- (cause) => new DatabaseError({ cause, operation: 'insert-account' }),
368
- )
369
- ```
312
+ // Reject null or undefined (preserves 0, false, and "")
313
+ const envName = Result.fromNullishOr(process.env.USER_NAME, () => "missing-name" as const);
370
314
 
371
- Continue from synchronous validation into async work with `asyncAndThen`:
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
+ );
372
323
 
373
- ```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)
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
+ );
383
329
  ```
384
330
 
385
- Once a workflow is already asynchronous, `andThen` accepts callbacks that return either `Result` or
386
- `ResultAsync`:
331
+ ### Chaining and transforming
332
+
333
+ Success (`Ok`) and failure (`Err`) travel on separate channels:
387
334
 
388
335
  ```ts
389
- const provisioned = createAccountAsync(input)
390
- .andThen(assignDefaultPlan)
391
- .andThen(sendWelcomeEmail)
392
- .map((account) => ({ account, status: 'ready' as const }))
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
393
345
  ```
394
346
 
395
- Choose the wrapper that matches the external boundary:
347
+ ### Functional pipelines (`pipe`, `flow`, `identity`, `constant`)
396
348
 
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)` |
405
-
406
- Prefer a factory with `tryResultAsync` when creating the promise can also throw synchronously.
407
-
408
- ## Reusable function composition
409
-
410
- Import `pipe`, `flow`, `identity`, and `constant` from the root package:
349
+ Result and Task transformations support fluent methods and curried functions inside `pipe`
350
+ and `flow`. `ResultAsync` also retains its existing functional forms:
411
351
 
412
352
  ```ts
413
- import { constant, flow, identity, ok, pipe } from 'resultar'
353
+ import { constant, flow, identity, pipe, Result } from "resultar";
354
+
355
+ const sanitize = flow(
356
+ (input: string) => input.trim(),
357
+ (input) => input.toLowerCase(),
358
+ );
414
359
 
415
- const normalize = flow((input: string) => input.trim(), (input) => input.toLowerCase())
416
- const name = pipe(' ADA ', normalize, (value) => ok(value))
417
- const label = name.match({ ok: identity, error: constant('Unavailable') })
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
+ );
418
367
  ```
419
368
 
420
- `pipe` applies zero through eight transformations; `value.pipe()` also accepts zero
421
- transformations. `flow` composes one through nine functions and preserves the first
422
- function's optional/rest parameters and explicit `this`. Later functions receive one
423
- value. Both compose synchronously: exceptions propagate, Promises pass through unchanged,
424
- and tasks execute only at a run boundary. `identity` preserves its argument's reference;
425
- `constant(value)` captures and returns that same value without copying it.
369
+ ---
426
370
 
427
- ## Lazy Workflows With ResultTask
371
+ <a id="linear-workflows-with-resultgen"></a>
372
+
373
+ ## Linear Workflows With Result And Task Generators
428
374
 
429
- `ResultTask<T, E, R>` is the lazy workflow primitive in Resultar. Creating one does not start the
430
- work; `runResult`, `runExit`, or `runPromise` executes it explicitly. `R` records required service
431
- tags and resource scope requirements. Run boundaries supply the root scope automatically and
432
- require an explicit environment for any remaining service tags.
433
-
434
- Unlike `ResultAsync`, a `ResultTask` is a reusable description of work rather than an already-started
435
- operation. Mapping, chaining, recovery, service provision, and generator composition all remain lazy.
436
-
437
- | Need | API |
438
- | --- | --- |
439
- | Create an immediate success or failure | `succeed`, `fail`, `fromResult` |
440
- | Defer synchronous work | `sync`, `try` |
441
- | Defer promise-producing work | `tryPromise` |
442
- | Transform, observe, or chain | `map`, `mapError`, `flatMap`, `andThen`, `tap`, `tapError`, `as`, `match` |
443
- | Recover typed failures | `catchAll`, `catchTag`, `catchTags` |
444
- | Write a linear lazy workflow | `gen` with `yield*`, `fn` for parameterized functions |
445
- | Declare and provide dependencies | `service`, `provideService`, `provideServices`, `provideServiceResolver` |
446
- | Own resources and await cleanup | `acquireRelease`, `acquireDisposable`, `scoped` |
447
- | Execute at the application boundary | `runExit`, `runResult`, `runPromise` |
448
- | Bridge to started promises | `fromResultAsync`, `toResultAsync`, `ResultAsync.fromTask` |
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.
449
378
 
450
379
  ```ts
451
- import { ResultTask } from 'resultar'
380
+ import { Result, ResultTask } from "resultar";
452
381
 
453
- const loadUser = (id: string) =>
454
- ResultTask.tryPromise({
455
- try: (signal) => fetch(`/users/${id}`, { signal }).then((response) => response.json()),
456
- catch: (cause) => new Error(`Could not load user: ${String(cause)}`),
457
- })
382
+ const validateAccount = Result.fn(function* (input: string) {
383
+ const email = yield* normalizeEmail(input);
384
+ yield* ensureAccountIsAvailable(email);
385
+ return email;
386
+ });
458
387
 
459
- const task = loadUser('user_123')
460
- const result = await ResultTask.runResult(task)
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
+ );
400
+
401
+ const result = await getOrCreateAccount(input).runResult();
461
402
  ```
462
403
 
463
- `sync` treats a thrown value as an unexpected defect. Use `try` or `tryPromise` when the boundary is
464
- expected to throw or reject and should map that cause into `E`. `tryPromise` receives the execution
465
- `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.
466
409
 
467
- Choose the execution boundary based on how much information the application needs:
410
+ ---
468
411
 
469
- | Boundary | Result |
470
- | --- | --- |
471
- | `runExit(task)` | Preserves `Success`, typed `Fail`, `Die`, `Interrupt`, and `Sequential` causes |
472
- | `runResult(task)` | Returns a single typed failure as `Err`; rejects defects, interruption, and composite causes |
473
- | `runPromise(task)` | Returns `T`; rejects every failure for integration with Promise-only APIs |
412
+ ## Recover And Handle Results At Boundaries
474
413
 
475
- The error type of `runExit` and `runResult` includes deferred release failures from the root scope.
476
- Interruption rejects with `AbortError`. A composite cause rejects with `ResultTaskCauseError`, whose
477
- `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:
478
417
 
479
418
  ```ts
480
- const controller = new AbortController()
481
- const exit = await ResultTask.runExit(loadUser('user_123'), {
482
- signal: controller.signal,
483
- })
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
+ );
484
423
 
485
- if (exit._tag === 'Failure' && exit.cause._tag === 'Die') {
486
- console.error('Unexpected defect', exit.cause.defect)
487
- }
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
+ });
488
429
  ```
489
430
 
490
- Instance methods and their static functional forms preserve laziness and infer combined error and
491
- environment types:
431
+ Use `orElse` when recovering from the entire error channel, or `orElseSucceed` / `unwrapOr` to fall back to a plain default value.
492
432
 
493
- ```ts
494
- const userName = loadUser('user_123')
495
- .map((user) => String(user.name))
496
- .andThen((name) => ResultTask.succeed(name.trim()))
497
- .catchAll((error) => ResultTask.succeed(`unavailable: ${error.message}`))
498
- ```
433
+ ### Exhaustive handling at application boundaries
499
434
 
500
- `catchAll` recovers a single typed failure. Defects and interruption bypass recovery. Composite
501
- causes bypass recovery as `Die(ResultTaskCauseError)` with the original tree on the error's `cause`;
502
- this prevents removed error types from escaping as typed failures. The equivalent functional forms
503
- 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:
504
436
 
505
- 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.
506
440
 
507
- ```ts
508
- const response = ResultTask.tryPromise((signal) => fetch(url, { signal }))
509
- // ResultTask<Response, unknown>: synchronous throws and rejections remain typed failures.
510
- ```
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()`.
511
442
 
512
- The callback runs only when the task executes. Use `tryPromise({ try, catch })` when you need a
513
- specific error type; both forms receive the runtime signal and preserve interruption semantics.
443
+ ### Strict Error-only boundaries (`StrictResult` & `StrictResultAsync`)
514
444
 
515
- Workflows can request typed services with `yield*` and receive them at the boundary. Pass the service
516
- 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>`.
517
449
 
518
- ```ts
519
- const Database = ResultTask.service<
520
- { findUser: (id: string) => Promise<string> }
521
- >()('Database')
522
-
523
- const taskWithDatabase = ResultTask.gen(function* () {
524
- const database = yield* Database
525
- return yield* ResultTask.tryPromise({
526
- try: () => database.findUser('user_123'),
527
- catch: () => 'database-error' as const,
528
- })
529
- })
530
-
531
- const resultWithDatabase = await ResultTask.runResult(taskWithDatabase, {
532
- services: { Database: { findUser: async () => 'Ada' } },
533
- })
534
- ```
450
+ ---
451
+
452
+ ## Lazy Workflows With ResultTask
535
453
 
536
- The environment requirement is part of the task type. A missing or misspelled `Database` property is
537
- a compile-time error at `runResult`, `runExit`, or `runPromise`. Dependencies can also be bound before
538
- 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(...)`.
539
465
 
540
466
  ```ts
541
- 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
+ });
542
474
 
543
- const readyWithOne = ResultTask.provideService(taskWithDatabase, Database, database)
544
- const readyWithAll = ResultTask.provideServices(taskWithDatabase, { Database: database })
475
+ const task = fetchUser("user_123"); // Nothing has executed yet!
545
476
 
546
- await ResultTask.runResult(readyWithOne)
547
- await ResultTask.runResult(readyWithAll)
477
+ // Execute explicitly at your application boundary (returns ResultAsync<User, UserLoadError>):
478
+ const result = await task.runResult();
548
479
  ```
549
480
 
550
- The direct form `ResultTask.service<DatabaseContract, 'Database'>('Database')` remains supported.
551
- The curried form can also be yielded inline: `yield* ResultTask.service<DatabaseContract>()('Database')`.
552
- Requirements belong to `R` in `ResultTask<T, E, R>`; `Result` has no service lookup API.
553
-
554
- Every tag creation has its own symbol identity. Inline tags resolve through named environments
555
- (`provideServices` or the DI module). `provideService(token, value)` supplies both an exact token binding and a named fallback.
556
- Reuse the same token to select its exact binding when identifiers collide; a separately created
557
- same-name tag can resolve the named fallback. Exact token bindings take precedence. For class services and explicit dependency maps, see
558
- [`Service.require` and `requires` in resultar-di](../di/README.md#class-services-and-explicit-requirements).
559
-
560
- Infrastructure such as a dependency-injection adapter can resolve service tags lazily with
561
- `provideServiceResolver(task, { Logger: () => loggerTask })`. Each required identifier has a
562
- typed lazy provider. Provider errors and external requirements remain in the resulting task type;
563
- resource-scope requirements also remain visible. All three providers (`provideService`,
564
- `provideServices`, `provideServiceResolver`) support data-first and curried (`.pipe`) forms.
565
- A provider whose value is incompatible with the required service contract is rejected at compile
566
- time, including through a reused curried provider; partial providers keep the remaining
567
- requirements until they are satisfied at the boundary.
568
-
569
- Adapters can use `ResultTask.makeScope()` to own resources across executions. Run tasks with
570
- `owner.use(task)` and execute `owner.close()` after consumers finish. Failed uses roll back their
571
- partial acquisitions. `ResultTask.memoize(task)` shares pending and successful executions and
572
- allows retry after failure; its first execution supplies the environment and resource scope.
573
-
574
- `ResultTask.gen` composes tasks and services linearly. On short-circuit, generator `finally` blocks
575
- 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.
576
495
 
577
496
  ```ts
578
- const program = ResultTask.gen(function* () {
579
- try {
580
- return yield* loadUser('user_123')
581
- } finally {
582
- yield* ResultTask.sync(() => logger.info('load-user finished'))
583
- }
584
- })
497
+ const task = ResultTask.sync(() => "ready");
498
+ const first = await task.runResult();
499
+ const second = await ResultTask.runResult(task); // Same contract, another execution
585
500
  ```
586
501
 
587
- Generator `finally` retains its existing replacement policy if its cleanup fails. Use resource
588
- scopes below when both the body and cleanup failure must be preserved.
589
-
590
- Generators also accept `yield* result` for plain `Result` values: `Ok` unwraps its value and
591
- `Err` short-circuits into `E`.
592
-
593
502
  ### Parameterized tasks and tagged recovery
594
503
 
595
- `ResultTask.fn` wraps a synchronous generator function. Calling the wrapper captures
596
- its arguments and receiver without executing the body. Each run creates a fresh iterator,
597
- with the same success/error/requirements inference and cleanup policy as `gen`:
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`:
598
505
 
599
506
  ```ts
600
- import { createTaggedError, pipe, ResultTask } from 'resultar'
507
+ import { createTaggedError, ok, ResultTask } from "resultar";
601
508
 
602
- class NotFound extends createTaggedError({ name: 'NotFound', message: 'User $id was not found' }) {}
603
- const Users = ResultTask.service<{
604
- find(id: string): ResultTask<string, NotFound>
605
- }>()('Users')
509
+ class UserNotFoundError extends createTaggedError({
510
+ name: "UserNotFoundError",
511
+ message: "User $id was not found",
512
+ }) {}
606
513
 
607
- const loadUser = ResultTask.fn(function* (id: string) {
608
- const users = yield* Users
609
- return yield* users.find(id)
610
- })
514
+ const UsersRepo = ResultTask.service<{
515
+ findById(id: string): ResultTask<{ id: string; name: string }, UserNotFoundError>;
516
+ }>()("UsersRepo");
611
517
 
612
- const recovered = pipe(loadUser('user_123'),
613
- ResultTask.catchTag('NotFound', (error: NotFound) => ResultTask.succeed(`Guest ${error.id}`)),
614
- )
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
+ });
615
537
  ```
616
538
 
617
- `task.catchTag(tag, handler)` narrows the handler's error to that tag. `task.catchTags(handlers)`
618
- allows partial recovery. Both also support `ResultTask.catchTag(task, tag, handler)` /
619
- `ResultTask.catchTags(task, handlers)` and curried forms for `pipe`. Unhandled tags remain in
620
- `E`; handler errors and requirements join the source channels. Scope requirements remain
621
- until closure. Handlers run only for simple `Fail` causes: defects and interruptions pass
622
- through, and composites retain the existing `catchAll` policy of `Die(ResultTaskCauseError)`.
623
- A throwing handler becomes a defect.
624
-
625
539
  ### Resource scopes and application lifecycle
626
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
+
627
543
  ```ts
628
- const connection = ResultTask.acquireRelease({
544
+ const dbResource = ResultTask.acquireRelease({
629
545
  acquire: ResultTask.tryPromise({
630
546
  try: (signal) => connectDatabase(signal),
631
547
  catch: (cause) => new ConnectionError({ cause }),
632
548
  }),
633
- release: (database, exit) => ResultTask.tryPromise({
634
- try: () => database.close(exit),
635
- 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
+ });
636
563
  }),
637
- })
564
+ );
565
+ ```
638
566
 
639
- const program = ResultTask.scoped(ResultTask.gen(function* () {
640
- const database = yield* connection
641
- return yield* ResultTask.tryPromise({
642
- try: (signal) => database.query('SELECT 1', signal),
643
- catch: (cause) => new QueryError({ cause }),
644
- })
645
- }))
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.
646
568
 
647
- const exit = await ResultTask.runExit(program)
648
- ```
569
+ ### Task concurrency and resilience
649
570
 
650
- For native `Disposable` or `AsyncDisposable` resources, use
651
- `ResultTask.acquireDisposable(acquireTask)` inside the same scopes. It prefers
652
- `Symbol.asyncDispose` when both protocols exist, awaits disposal once in LIFO order,
653
- and registers nothing when acquisition fails. Disposal throws/rejections become `Die`;
654
- body and disposal failures retain their sequential cause tree. Cancellation does not
655
- interrupt cleanup. This helper adds a scope requirement to `R`, just like `acquireRelease`.
656
-
657
- Every run owns a root scope. `scoped` closes a child scope before the next task continues. Successful
658
- acquisitions register finalizers in LIFO order; every finalizer is awaited once, even after a body
659
- failure, defect, or interruption. Failed acquisition does not register its release. Earlier resources
660
- are still released if a later acquisition fails. Each finalizer receives the same region exit,
661
- before release failures are appended. Services provided around acquisition remain available during
662
- release, including services used only by the release callback.
663
-
664
- `acquireRelease` tracks deferred errors with `ResultTaskScope<ReleaseError>` in `R`. `scoped` removes
665
- that requirement and adds its errors to `E`; run boundaries do the same for the root scope. Service
666
- provision and `catchAll` before scope closure cannot erase pending release failures. To recover a
667
- single release failure, put `catchAll` after `scoped`.
668
-
669
- If use succeeds but release fails, the failure is returned as `Err`. If both fail, `runExit` retains
670
- them as `Sequential(useCause, releaseCause)`; subsequent release failures are appended in order.
671
- Release callbacks that throw produce defects. Resources acquired during a finalizer belong to a
672
- private cleanup scope, which is closed before the next finalizer starts.
673
-
674
- Cancellation is cooperative: tasks check the signal before starting, and promise operations receive
675
- it. Rejection after abort is classified as interruption. The runtime waits for in-flight operations
676
- to settle so a resource acquired during cancellation can still be registered and released. Cleanup
677
- uses a fresh, non-aborted signal. An operation or finalizer that never settles can therefore keep
678
- the run pending; there is no preemptive cancellation, scheduler, or `Fiber` runtime in this slice.
679
-
680
- For application composition, keep ordinary factories and existing `StrictResultAsync` use cases.
681
- Use service tags only when a program needs requirements supplied at execution. Put the **whole server
682
- lifetime** inside the resource scope: acquire database, session, then HTTP; wait for shutdown; drain
683
- HTTP, close session, then close database. Returning a server from a completed scope would return
684
- already-released resources. TypeScript does not enforce resource reference lifetimes.
685
-
686
- See the runnable [application lifecycle example](../../examples/resultar/src/application-lifecycle.ts)
687
- and its [smoke test](../../examples/resultar/scripts/lifecycle-smoke.ts). Run `pnpm run example:resultar`
688
- from the workspace root. Its factory and resource contracts return `ResultTask` with database,
689
- session, and HTTP error types. Drivers expose Promises only inside `ResultTask.tryPromise` adapters,
690
- which receive the runtime signal without threading it through every application interface.
691
- Concrete adapters must clean up partial acquisitions before returning a
692
- failure and implement request draining, SSE termination, and suitable deadlines.
693
-
694
- `ResultAsync.withResource` keeps its existing best-effort release behavior. Existing `ResultAsync`
695
- instances have already started; wrapping one does not make it lazy or automatically cancelable.
696
- Use `ResultTask.fromResultAsync` to run a `ResultAsync` inside a task boundary: pass a
697
- `(signal) => ResultAsync` factory to defer creation until execution, or an existing instance to
698
- capture an already-started execution. Use `ResultTask.toResultAsync(task)` — or the
699
- `ResultAsync.fromTask(task)` alias — to start a requirement-free task immediately on the default
700
- runtime and consume it as `ResultAsync`.
701
-
702
- ## Production Async Policies
703
-
704
- Resultar models common resilience policy in the expected error channel. These helpers use lazy tasks
705
- so retries can start fresh work and racing helpers can pass a cooperative abort signal.
706
-
707
- ### Timeout stays typed
571
+ `ResultTask` pipelines share the active `AbortSignal`, `Clock`, and resource scope across retries, timeouts, races, bounded collections, and child fibers:
708
572
 
709
573
  ```ts
710
- class AccountTimeoutError extends createTaggedError({
711
- name: 'AccountTimeoutError',
712
- message: 'Account $accountId timed out after $timeoutMs milliseconds',
713
- }) {}
574
+ import { Clock, Fiber, ResultTask, Retry } from "resultar";
714
575
 
715
- const account = ResultAsync.timeout(
716
- (signal) => loadAccount(accountId, { signal }),
717
- {
718
- timeoutMs: 1_500,
719
- onTimeout: () => new AccountTimeoutError({ accountId, timeoutMs: 1_500 }),
720
- },
721
- )
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;
722
585
  ```
723
586
 
724
- If the timer wins, the timeout is returned as `Err<AccountTimeoutError>` and the task receives an
725
- 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)`.
726
589
 
727
- ### Retry transient failures
590
+ ### Portable retry policies
728
591
 
729
- ```ts
730
- const account = ResultAsync.retry(
731
- (attempt, signal) => loadAccount(accountId, { attempt, signal }),
732
- {
733
- times: 2,
734
- delayMs: ({ nextAttempt }) => nextAttempt * 100,
735
- jittered: 0.2,
736
- while: (error) => error._tag === 'DatabaseBusyError',
737
- },
738
- )
739
- ```
740
-
741
- `times` is the number of retries after the first attempt. Use `retryOrElse` when retry exhaustion
742
- 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:
743
594
 
744
595
  ```ts
745
- const account = ResultAsync.retryOrElse(
746
- (attempt, signal) => loadAccount(accountId, { attempt, signal }),
747
- {
748
- times: 2,
749
- orElse: () => loadCachedAccount(accountId),
750
- },
751
- )
752
- ```
596
+ import { ResultTask, Retry } from "resultar";
753
597
 
754
- ### Race equivalent providers
598
+ const transientPolicy = Retry.policy({
599
+ times: 3,
600
+ delayMs: 100,
601
+ backoff: 2,
602
+ jittered: 0.2,
603
+ });
755
604
 
756
- ```ts
757
- const account = ResultAsync.race(
758
- (signal) => loadAccountFromPrimary(accountId, { signal }),
759
- (signal) => loadAccountFromReplica(accountId, { signal }),
760
- )
605
+ const taskWithRetry = fetchUser("user_123").retry(transientPolicy);
606
+ const result = await taskWithRetry.runResult();
761
607
  ```
762
608
 
763
- `race` returns the first success and keeps waiting after an early failure. Use `raceFirst` when the
764
- first completed success or failure should win, or `raceAll` for several equivalent providers.
609
+ ### Inspect failure causes
765
610
 
766
- ### 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:
767
612
 
768
613
  ```ts
769
- const imported = ResultAsync.forEach(
770
- accountInputs,
771
- (input) => createAccountAsync(input.email),
772
- { concurrency: 8, discard: true },
773
- )
774
- ```
614
+ import { Cause, ResultTask } from "resultar";
775
615
 
776
- `forEach` is sequential by default and stops scheduling new work after the first `Err`. Pass a
777
- positive concurrency number for bounded work or `"unbounded"` to start every mapped task.
616
+ const exit = await queryProgram.runExit();
778
617
 
779
- Use mapped `validateAll` when every independent item should run and all failures should be returned:
780
-
781
- ```ts
782
- const validated = ResultAsync.validateAll(
783
- accountInputs,
784
- (input) => normalizeEmail(input.email).asyncMap(async (email) => email),
785
- { concurrency: 8 },
786
- )
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
+ }
787
623
  ```
788
624
 
789
- ### 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).
790
626
 
791
- ```ts
792
- const account = ResultAsync.withResource({
793
- acquire: (signal) => databasePool.connect({ signal }),
794
- use: (connection, signal) => connection.insertAccount(input, { signal }),
795
- release: (connection) => connection.close(),
796
- })
797
- ```
627
+ ---
798
628
 
799
- After successful acquisition, `release` runs when `use` succeeds, returns `Err`, or rejects. For
800
- stream-shaped work, use native `AsyncIterable<Result<T, E>>`; Resultar intentionally does not add a
801
- 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>
802
632
 
803
- See the full guides for
804
- [racing and timeout](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#concurrent-racing-and-timeouts),
805
- [retry](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#retrying-async-work),
806
- [concurrency](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#async-concurrency-mapping),
807
- and [resource cleanup](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#resourceful-async-iterables).
633
+ ## In-Flight `ResultAsync` & Deprecated Eager Compatibility
808
634
 
809
- ## 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`).
810
644
 
811
- Use `Result.gen` when a longer chain reads better as linear code. `yield*` extracts the `Ok` value and
812
- short-circuits on the first `Err`. `safeTry` remains an exact compatibility alias.
645
+ ```sh
646
+ pnpm add resultar
647
+ ```
813
648
 
814
- ```ts
815
- import { Result } from 'resultar'
816
- 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.
817
660
 
818
- const createAccountLinear = (
819
- input: string,
820
- ): ResultAsync<Account, CreateAccountAsyncError> =>
821
- Result.gen(async function* () {
822
- const email = yield* normalizeEmail(input)
823
- yield* ensureAccountIsAvailable(email)
661
+ ### Wrapping external async boundaries (eager compatibility)
824
662
 
825
- return persistAccount(email)
826
- })
827
- ```
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)` |
828
669
 
829
- The object form can map unexpected throws from inside the generator while leaving yielded `Err`
830
- 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:
831
675
 
832
676
  ```ts
833
- const account = Result.gen({
834
- async *try() {
835
- const email = yield* normalizeEmail(input)
836
- return persistAccount(email)
837
- },
838
- catch: (cause) => new AccountServiceError({ cause, email: input }),
839
- })
840
- ```
677
+ import { ResultAsync } from "resultar";
841
678
 
842
- 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
+ });
843
684
 
844
- ## 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
+ );
845
696
 
846
- 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
+ );
847
702
 
848
- 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
+ );
849
709
 
850
- ```ts
851
- const account = createAccountAsync(input).catchTag(
852
- 'AccountAlreadyExistsError',
853
- (error) => findAccountByEmail(String(error.email)),
854
- )
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
+ });
855
716
  ```
856
717
 
857
- Recover several tagged errors with `catchTags`:
718
+ ---
858
719
 
859
- ```ts
860
- const account = createAccountAsync(input).catchTags({
861
- AccountAlreadyExistsError: (error) => findAccountByEmail(String(error.email)),
862
- InvalidEmailError: (error) =>
863
- ok({ email: String(error.email), id: 'draft_account' }),
864
- })
865
- ```
720
+ ### Async Result generators and bridges
866
721
 
867
- Unhandled tags remain in the error channel. Use `mapErr` when the failure should be translated and
868
- `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`:
869
724
 
870
725
  ```ts
871
- const account = loadCachedAccount(accountId)
872
- .orElse(() => loadAccount(accountId))
873
- .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;
874
744
  ```
875
745
 
876
- Use recovery inside a workflow. At an application boundary, consume the final result with `match`,
877
- `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.
878
752
 
879
- ## 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.
880
758
 
881
- Boundary code turns a Result into an HTTP response, queue acknowledgement, CLI exit code, log entry,
882
- or UI state.
759
+ ---
883
760
 
884
- 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:
885
764
 
886
765
  ```ts
887
- const response = await createAccountAsync(input).matchTags(
888
- (account) => ({
889
- body: account,
890
- statusCode: 201,
891
- }),
892
- {
893
- AccountAlreadyExistsError: (error) => ({
894
- body: { code: error._tag, message: error.message },
895
- statusCode: 409,
896
- }),
897
- DatabaseError: (error) => ({
898
- body: { code: error._tag, message: error.message },
899
- statusCode: 503,
900
- }),
901
- InvalidEmailError: (error) => ({
902
- body: { code: error._tag, message: error.message },
903
- statusCode: 400,
904
- }),
905
- },
906
- )
907
- ```
766
+ import { Match } from "resultar";
908
767
 
909
- If a handler is missing, TypeScript reports it. If only selected errors need special handling, use
910
- `matchTagsPartial` with a fallback. Use plain `match` when the distinction between individual error
911
- tags does not matter.
768
+ type DomainEvent = { _tag: "UserCreated"; id: string } | { _tag: "UserDeleted"; count: number };
912
769
 
913
- Avoid unwrapping in normal application flows. `unwrapOr`, `unwrapOrThrow`, `_unsafeUnwrap`, and
914
- `_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();
915
775
 
916
- ## 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
+ ```
917
782
 
918
- `Result<T, E>` intentionally allows any failure type. Strings, enums, and small objects can be useful
919
- inside narrow local workflows.
783
+ ### Reusable HTTP error handlers with `Match.context`
920
784
 
921
- 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:
922
786
 
923
787
  ```ts
924
- import type { StrictResult, StrictResultAsync } from 'resultar'
788
+ import type { Context } from "hono";
789
+ import { Match } from "resultar";
925
790
 
926
- const validateAccount = (
927
- input: string,
928
- ): 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
+ });
929
798
 
930
- const provisionAccount = (
931
- input: string,
932
- ): 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
+ });
933
805
  ```
934
806
 
935
- `StrictResult<T, E extends Error>` and `StrictResultAsync<T, E extends Error>` are type-only aliases.
936
- They keep the same Resultar API while documenting that failures carry standard `Error` behavior:
937
- 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
+ ---
938
815
 
939
816
  ## Prevent Ignored Results
940
817
 
941
- 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:
942
819
 
943
820
  ```sh
944
821
  pnpm add -D resultar-check
945
822
  ```
946
823
 
947
- Use the CLI as the authoritative local and CI check:
948
-
949
- ```json
950
- {
951
- "scripts": {
952
- "check": "resultar-check"
953
- }
954
- }
955
- ```
956
-
957
- The CLI uses TypeScript-Go to run compiler and Resultar diagnostics over the same `tsconfig.json`.
958
- Configure its rules in the project file:
824
+ Add the rule configuration to `tsconfig.json`:
959
825
 
960
826
  ```json
961
827
  {
@@ -971,108 +837,94 @@ Configure its rules in the project file:
971
837
  }
972
838
  ```
973
839
 
974
- The `plugins` entry is configuration consumed by the native CLI and stdio LSP server; it does not
975
- install an editor extension. Run `resultar-check lsp` from an editor language-server configuration.
976
- 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
977
845
 
978
- See the
979
- [`resultar-check` guide](https://github.com/inaiat/resultar/blob/main/packages/check/README.md)
980
- for all diagnostics, severities, modes, and ignore patterns.
846
+ Resultar's companion packages integrate directly with the core primitives:
981
847
 
982
- ## 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) |
983
857
 
984
- Resultar's Fetch-first request packages return `ResultAsync` values for request creation, network,
985
- 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.
986
859
 
987
- | Package | Use it when | Guide |
988
- | --- | --- | --- |
989
- | [`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) |
990
- | [`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) |
991
- | [`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:
992
862
 
993
- The adapters delegate transport, JSON parsing, retries, and error mapping to `resultar-request` and
994
- re-export its public request types. Install only the core request package and schema adapter your
995
- 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
+ ---
996
883
 
997
884
  ## API Decision Guide
998
885
 
999
- | Need | Use |
1000
- | --- | --- |
1001
- | Create success or failure | `ok`, `err`, `okAsync`, `errAsync` |
1002
- | Create an `Ok(undefined)` | `unit`, `unitAsync` |
1003
- | Transform success | `map`, `asyncMap`, `as` |
1004
- | Add a fallible condition | `filterOrElse` |
1005
- | Transform failure | `mapErr` |
1006
- | Continue fallible work | `andThen`, `asyncAndThen` |
1007
- | Recover from failure | `orElse`, `catchTag`, `catchTags` |
1008
- | Wrap throwing or rejecting code | `tryResult`, `tryResultAsync`, `fromPromise` |
1009
- | Write linear Result code | `Result.gen` (`safeTry` compatibility alias) |
1010
- | Describe reusable lazy work | `ResultTask.succeed`, `sync`, `try`, `tryPromise` |
1011
- | Compose or recover lazy work | `ResultTask.map`, `flatMap`, `andThen`, `catchAll`, `gen` |
1012
- | Require or bind typed services | `ResultTask.service`, `provideService`, `provideServices`, `provideServiceResolver` |
1013
- | Execute lazy work explicitly | `ResultTask.runExit`, `runResult`, `runPromise` |
1014
- | Bridge to started promises | `ResultTask.fromResultAsync`, `toResultAsync`, `ResultAsync.fromTask` |
1015
- | Handle a final boundary | `match`, `matchTags`, `matchTagsPartial` |
1016
- | Combine independent results | `zip`, `combine`, `combineWithAllErrors` |
1017
- | Try ordered fallback candidates | `firstSuccessOf` |
1018
- | Process async collections | `ResultAsync.forEach`, `ResultAsync.validateAll` |
1019
- | Race concurrent tasks | `ResultAsync.race`, `raceAll`, `raceFirst` |
1020
- | Apply a timeout | `ResultAsync.timeout` |
1021
- | Retry transient work | `ResultAsync.retry`, `retryOrElse` |
1022
- | Pair acquisition and cleanup | `ResultAsync.withResource` |
1023
- | Observe without changing the result | `tap`, `tapError`, `log` |
1024
- | Default deliberately at an edge | `unwrapOr` |
1025
- | Throw deliberately at an edge | `unwrapOrThrow` |
1026
-
1027
- The
1028
- [complete API map](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#api-map)
1029
- covers overloads, aliases, callback semantics, collection helpers, conditional helpers, tagged
1030
- reasons, disposable results, and compatibility APIs.
1031
-
1032
- ## Coding agents
1033
-
1034
- Each npm build includes `dist/llms.txt`, `dist/agent/SKILL.md`, focused references, a compiled
1035
- core workflow example, and `dist/agent/versions.json`. Start from these installed files so the
1036
- guide matches the package version. Companion package READMEs take precedence when their installed
1037
- versions differ from the recorded companion versions. The online `main` guide tracks development.
1038
-
1039
- The guide distinguishes `Result.gen` from `ResultTask.gen`, immediate from lazy execution,
1040
- 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
+ ---
1041
914
 
1042
915
  ## More Documentation
1043
916
 
1044
- - [Full Resultar guide](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md)
1045
- - [ResultTask core RFC](https://github.com/inaiat/resultar/blob/main/docs/rfcs/rfc-0001-result-task-core.md)
1046
- - [Runnable core cookbook](https://github.com/inaiat/resultar/tree/main/examples/resultar)
1047
- - [Catching and recovering errors](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#catching-and-recovering-errors)
1048
- - [Safe Try](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#safe-try)
1049
- - [Validation error recipes](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#validation-error-recipes)
1050
- - [Coming from other error-handling styles](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#coming-from-other-styles)
1051
- - [Public exports and compatibility aliases](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#public-entry-point)
1052
- - [Type-safe error handling article](https://github.com/inaiat/resultar/blob/main/articles/en/type-safe.md)
1053
- - [Artigo sobre tratamento de erros type-safe](https://github.com/inaiat/resultar/blob/main/articles/pt/type-safe.md)
1054
-
1055
- Resultar is also published on [JSR as `@inaiat/resultar`](https://jsr.io/@inaiat/resultar).
1056
-
1057
- ## Limitations
1058
-
1059
- ResultTask implements the RFC 0001 lazy core, generator with services, and resource scopes. The
1060
- following are explicitly out of scope for this slice:
1061
-
1062
- - No `Stream`, STM, caching, clustering, RPC, or platform layer; this is not a full Effect API port.
1063
- - No implicit global runtime: every `run*` call creates its own root scope and signal, and simple
1064
- tasks need no services or configuration.
1065
- - No fibers or structured concurrency yet: `forkChild`/`join`/`interrupt`, task `race`/`timeout`,
1066
- and uniform `all`/`forEach` over the task runtime are still Phase 3. The `Cause` model covers
1067
- `Fail`, `Die`, `Interrupt`, and `Sequential`; `Parallel` is reserved for that future work. Use the
1068
- `ResultAsync` race, timeout, retry, and concurrency helpers in the meantime — they keep their own
1069
- eager, per-helper protocols.
1070
- - No `Schedule`, schedule-based retry, or injectable clock yet (Phase 4).
1071
- - No `Layer` system in the core: service graphs with their own lifecycle belong to the optional
1072
- `resultar-di` package, and the core does not depend on it.
1073
- - `ResultAsync` stays eager and awaitable. `fromResultAsync` on an existing instance captures an
1074
- already-started execution; only the factory form defers creation.
1075
- - 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.
1076
928
 
1077
929
  ## License
1078
930