resultar 3.9.1 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +676 -766
- package/dist/agent/SKILL.md +257 -74
- package/dist/agent/examples/workflow.mjs +5 -3
- package/dist/agent/examples/workflow.mts +13 -8
- package/dist/agent/references/api.md +350 -91
- package/dist/agent/references/integrations.md +64 -10
- package/dist/agent/references/patterns.md +232 -74
- package/dist/agent/references/review-checklist.md +60 -19
- package/dist/agent/references/services.md +26 -6
- package/dist/agent/versions.json +8 -8
- package/dist/index.d.ts +3204 -943
- package/dist/index.js +5604 -2215
- package/dist/llms.txt +1 -1
- package/package.json +9 -9
package/README.md
CHANGED
|
@@ -1,903 +1,827 @@
|
|
|
1
1
|
# Resultar
|
|
2
2
|
|
|
3
|
-
Typed, composable error handling for TypeScript
|
|
3
|
+
**Typed, composable error handling and resilient workflows for TypeScript — from a single `Result<T, E>` to lazy, dependency-injected `ResultTask<T, E, R>`.**
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
[](https://nodejs.org)
|
|
6
|
+
[](https://github.com/inaiat/resultar/blob/main/packages/resultar/package.json)
|
|
7
|
+
[](https://github.com/inaiat/resultar/blob/main/packages/check/README.md)
|
|
8
|
+
[](https://github.com/inaiat/resultar/blob/main/LICENSE)
|
|
7
9
|
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
and adds Resultar-specific tagged errors, production async helpers, exhaustive tagged boundaries,
|
|
37
|
-
and optional TypeScript-backed diagnostics.
|
|
16
|
+
---
|
|
38
17
|
|
|
39
|
-
##
|
|
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
|
-
|
|
20
|
+
### Before: Hidden failures
|
|
60
21
|
|
|
61
|
-
|
|
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
|
-
|
|
95
|
-
}
|
|
30
|
+
return port;
|
|
31
|
+
};
|
|
96
32
|
```
|
|
97
33
|
|
|
98
|
-
|
|
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
|
|
102
|
-
import type { Result } from 'resultar'
|
|
39
|
+
import { createTaggedError, ok, type Result } from "resultar";
|
|
103
40
|
|
|
104
41
|
class InvalidPortError extends createTaggedError({
|
|
105
|
-
name:
|
|
106
|
-
message:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
143
|
-
message:
|
|
61
|
+
name: "InvalidEmailError",
|
|
62
|
+
message: "Invalid email $email",
|
|
144
63
|
}) {}
|
|
145
64
|
|
|
146
|
-
class
|
|
147
|
-
name:
|
|
148
|
-
message:
|
|
65
|
+
class AccountExistsError extends createTaggedError({
|
|
66
|
+
name: "AccountExistsError",
|
|
67
|
+
message: "Account $email already exists",
|
|
149
68
|
}) {}
|
|
150
69
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
160
|
-
const normalizeEmail = (
|
|
161
|
-
const email =
|
|
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
|
-
|
|
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
|
-
|
|
172
|
-
|
|
137
|
+
```sh
|
|
138
|
+
pnpm add resultar
|
|
139
|
+
# or
|
|
140
|
+
npm install resultar
|
|
173
141
|
```
|
|
174
142
|
|
|
175
|
-
|
|
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
|
-
|
|
178
|
-
type CreateAccountError = InvalidEmailError | AccountAlreadyExistsError
|
|
147
|
+
---
|
|
179
148
|
|
|
180
|
-
|
|
181
|
-
normalizeEmail(input).andThen(ensureAccountIsAvailable).andThen(buildAccount)
|
|
182
|
-
```
|
|
149
|
+
## What's New in v4
|
|
183
150
|
|
|
184
|
-
|
|
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
|
-
|
|
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
|
-
|
|
190
|
-
account.value.email
|
|
191
|
-
}
|
|
160
|
+
---
|
|
192
161
|
|
|
193
|
-
|
|
194
|
-
account.error.message
|
|
195
|
-
}
|
|
196
|
-
```
|
|
162
|
+
## Adopt Gradually: Three Primitives, One Mental Model
|
|
197
163
|
|
|
198
|
-
|
|
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
|
-
```
|
|
201
|
-
|
|
202
|
-
(
|
|
203
|
-
|
|
204
|
-
)
|
|
166
|
+
```mermaid
|
|
167
|
+
flowchart LR
|
|
168
|
+
R["<b>1. Result<T, E></b><br/>Synchronous value (Ok / Err)<br/>Zero async overhead"]
|
|
169
|
+
RT["<b>2. ResultTask<T, E, R></b><br/>Default for new async workflows<br/>Lazy, repeatable, explicit runners"]
|
|
170
|
+
RA["<b>ResultAsync<T, E></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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
214
|
-
const accountId = createAccount('person@example.com').map((account) => account.id)
|
|
215
|
-
```
|
|
195
|
+
### Sync-First, Pay-As-You-Go Execution Engine
|
|
216
196
|
|
|
217
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
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
|
-
|
|
206
|
+
---
|
|
236
207
|
|
|
237
|
-
|
|
238
|
-
const createCompanyAccount = (input: string) =>
|
|
239
|
-
requireCompanyEmail(input).andThen(ensureAccountIsAvailable).andThen(buildAccount)
|
|
240
|
-
```
|
|
208
|
+
## Contents
|
|
241
209
|
|
|
242
|
-
|
|
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
|
-
|
|
245
|
-
class AccountServiceError extends createTaggedError({
|
|
246
|
-
name: 'AccountServiceError',
|
|
247
|
-
message: 'Account service failed for $email',
|
|
248
|
-
}) {}
|
|
228
|
+
---
|
|
249
229
|
|
|
250
|
-
|
|
251
|
-
(cause) => new AccountServiceError({ cause, email: input }),
|
|
252
|
-
)
|
|
253
|
-
```
|
|
230
|
+
## Tagged Errors
|
|
254
231
|
|
|
255
|
-
|
|
232
|
+
`createTaggedError` builds real `Error` subclasses with a stable `_tag` discriminant, automatic `$variable` message interpolation, and structured serialization:
|
|
256
233
|
|
|
257
234
|
```ts
|
|
258
|
-
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
Use `pipe` when a result transform should be named and reused:
|
|
235
|
+
import { createTaggedError } from "resultar";
|
|
262
236
|
|
|
263
|
-
|
|
264
|
-
|
|
237
|
+
class AccountAlreadyExistsError extends createTaggedError({
|
|
238
|
+
name: "AccountAlreadyExistsError",
|
|
239
|
+
message: "Account $email already exists",
|
|
240
|
+
}) {}
|
|
265
241
|
|
|
266
|
-
const
|
|
267
|
-
result.tap((account) => logger.info({ accountId: account.id }, 'account ready'))
|
|
242
|
+
const error = new AccountAlreadyExistsError({ email: "ada@example.com" });
|
|
268
243
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
309
|
-
name:
|
|
310
|
-
message:
|
|
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
|
|
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
|
-
|
|
322
|
-
|
|
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
|
|
280
|
+
import { createTaggedError, isRedacted, redact, revealRedacted } from "resultar";
|
|
326
281
|
|
|
327
282
|
class TokenRejectedError extends createTaggedError({
|
|
328
|
-
name:
|
|
329
|
-
message:
|
|
283
|
+
name: "TokenRejectedError",
|
|
284
|
+
message: "Token $token was rejected",
|
|
330
285
|
}) {}
|
|
331
286
|
|
|
332
287
|
const error = new TokenRejectedError({
|
|
333
|
-
token: redact(
|
|
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) // '
|
|
295
|
+
revealRedacted(error.token); // 'sk_live_secret' (explicit access for authorized code)
|
|
340
296
|
}
|
|
341
297
|
```
|
|
342
298
|
|
|
343
|
-
|
|
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
|
-
|
|
301
|
+
---
|
|
349
302
|
|
|
350
|
-
|
|
351
|
-
composition style.
|
|
303
|
+
## Synchronous Workflows
|
|
352
304
|
|
|
353
|
-
|
|
305
|
+
### Creating Results
|
|
306
|
+
|
|
307
|
+
In addition to `ok(value)` and `err(error)`, Resultar provides helpers to lift nullable values, predicates, and throwing functions into `Result`:
|
|
354
308
|
|
|
355
309
|
```ts
|
|
356
|
-
import {
|
|
357
|
-
import type { ResultAsync } from 'resultar'
|
|
310
|
+
import { ok, pipe, Result, tryResult } from "resultar";
|
|
358
311
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
message: 'Database $operation failed',
|
|
362
|
-
}) {}
|
|
312
|
+
// Reject null or undefined (preserves 0, false, and "")
|
|
313
|
+
const envName = Result.fromNullishOr(process.env.USER_NAME, () => "missing-name" as const);
|
|
363
314
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
315
|
+
// Narrow or validate with a predicate (direct or curried in pipe)
|
|
316
|
+
const positivePort = pipe(
|
|
317
|
+
3000,
|
|
318
|
+
Result.liftPredicate(
|
|
319
|
+
(n: number) => n > 0,
|
|
320
|
+
(n) => `invalid port: ${n}` as const,
|
|
321
|
+
),
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
// Wrap synchronous code that might throw
|
|
325
|
+
const parsedJson = tryResult(
|
|
326
|
+
() => JSON.parse(rawInput) as Record<string, unknown>,
|
|
327
|
+
(cause) => new InvalidJsonError({ cause }),
|
|
328
|
+
);
|
|
369
329
|
```
|
|
370
330
|
|
|
371
|
-
|
|
331
|
+
### Chaining and transforming
|
|
332
|
+
|
|
333
|
+
Success (`Ok`) and failure (`Err`) travel on separate channels:
|
|
372
334
|
|
|
373
335
|
```ts
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
)
|
|
382
|
-
|
|
336
|
+
const accountEmail = normalizeEmail(input)
|
|
337
|
+
.filterOrElse(
|
|
338
|
+
(email) => email.endsWith("@company.com"),
|
|
339
|
+
(email) => new UnsupportedDomainError({ domain: email.split("@")[1] ?? "unknown" }),
|
|
340
|
+
)
|
|
341
|
+
.andThen(ensureAccountIsAvailable) // runs only on Ok; merges error unions
|
|
342
|
+
.map((email) => ({ id: "acct_1", email })) // transforms Ok value
|
|
343
|
+
.inspect((acct) => logger.info(acct.id)) // best-effort side effect on Ok
|
|
344
|
+
.mapErr((cause) => new AccountError({ cause })); // transforms Err value
|
|
383
345
|
```
|
|
384
346
|
|
|
385
|
-
|
|
386
|
-
|
|
347
|
+
### Functional pipelines (`pipe`, `flow`, `identity`, `constant`)
|
|
348
|
+
|
|
349
|
+
Result and Task transformations support fluent methods and curried functions inside `pipe`
|
|
350
|
+
and `flow`. `ResultAsync` also retains its existing functional forms:
|
|
387
351
|
|
|
388
352
|
```ts
|
|
389
|
-
|
|
390
|
-
.andThen(assignDefaultPlan)
|
|
391
|
-
.andThen(sendWelcomeEmail)
|
|
392
|
-
.map((account) => ({ account, status: 'ready' as const }))
|
|
393
|
-
```
|
|
353
|
+
import { constant, flow, identity, pipe, Result } from "resultar";
|
|
394
354
|
|
|
395
|
-
|
|
355
|
+
const sanitize = flow(
|
|
356
|
+
(input: string) => input.trim(),
|
|
357
|
+
(input) => input.toLowerCase(),
|
|
358
|
+
);
|
|
396
359
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
360
|
+
const label = pipe(
|
|
361
|
+
" ADA@COMPANY.COM ",
|
|
362
|
+
sanitize,
|
|
363
|
+
normalizeEmail,
|
|
364
|
+
Result.map((email) => `User: ${email}`),
|
|
365
|
+
Result.match({ ok: identity, err: constant("Invalid user") }),
|
|
366
|
+
);
|
|
367
|
+
```
|
|
405
368
|
|
|
406
|
-
|
|
369
|
+
---
|
|
407
370
|
|
|
408
|
-
|
|
371
|
+
<a id="linear-workflows-with-resultgen"></a>
|
|
409
372
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
Unlike `ResultAsync`, a `ResultTask` is a reusable description of work rather than an already-started
|
|
416
|
-
operation. Mapping, chaining, recovery, service provision, and generator composition all remain lazy.
|
|
417
|
-
|
|
418
|
-
| Need | API |
|
|
419
|
-
| --- | --- |
|
|
420
|
-
| Create an immediate success or failure | `succeed`, `fail`, `fromResult` |
|
|
421
|
-
| Defer synchronous work | `sync`, `try` |
|
|
422
|
-
| Defer promise-producing work | `tryPromise` |
|
|
423
|
-
| Transform, observe, or chain | `map`, `mapError`, `flatMap`, `andThen`, `tap`, `tapError`, `as`, `match` |
|
|
424
|
-
| Recover typed failures | `catchAll` |
|
|
425
|
-
| Write a linear lazy workflow | `gen` with `yield*` |
|
|
426
|
-
| Declare and provide dependencies | `service`, `provideService`, `provideServices`, `provideServiceResolver` |
|
|
427
|
-
| Own resources and await cleanup | `acquireRelease`, `scoped` |
|
|
428
|
-
| Execute at the application boundary | `runExit`, `runResult`, `runPromise` |
|
|
429
|
-
| Bridge to started promises | `fromResultAsync`, `toResultAsync`, `ResultAsync.fromTask` |
|
|
373
|
+
## Linear Workflows With Result And Task Generators
|
|
374
|
+
|
|
375
|
+
Use `Result.gen` / `Result.fn` for synchronous domain steps, and `ResultTask.gen` /
|
|
376
|
+
`ResultTask.fn` for new asynchronous workflows. Both use plain success returns;
|
|
377
|
+
`yield*` unwraps successes and short-circuits expected failures.
|
|
430
378
|
|
|
431
379
|
```ts
|
|
432
|
-
import { ResultTask } from
|
|
380
|
+
import { Result, ResultTask } from "resultar";
|
|
433
381
|
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
382
|
+
const validateAccount = Result.fn(function* (input: string) {
|
|
383
|
+
const email = yield* normalizeEmail(input);
|
|
384
|
+
yield* ensureAccountIsAvailable(email);
|
|
385
|
+
return email;
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// persistAccount and findAccountByEmail return ResultTask<Account, ...>.
|
|
389
|
+
const createAccount = ResultTask.fn(function* (input: string) {
|
|
390
|
+
const email = yield* validateAccount(input);
|
|
391
|
+
return yield* persistAccount(email);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
const getOrCreateAccount = ResultTask.fn(
|
|
395
|
+
function* (input: string) {
|
|
396
|
+
return yield* createAccount(input);
|
|
397
|
+
},
|
|
398
|
+
ResultTask.catchTag("AccountExistsError", (error) => findAccountByEmail(String(error.email))),
|
|
399
|
+
);
|
|
439
400
|
|
|
440
|
-
const
|
|
441
|
-
const result = await ResultTask.runResult(task)
|
|
401
|
+
const result = await getOrCreateAccount(input).runResult();
|
|
442
402
|
```
|
|
443
403
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
`
|
|
404
|
+
- Return `value` for success; use `return yield* step()` or `return yield* MyError.err(...)` for a final fallible step.
|
|
405
|
+
- Task generators are synchronous `function*` bodies describing asynchronous steps. Compose those steps with `yield*`; execute the completed Task with an explicit runner.
|
|
406
|
+
- Core `Result.gen` / `Result.fn` and legacy `safeTry` support sync and eager async bodies; see [compatibility](#in-flight-resultasync--deprecated-eager-compatibility).
|
|
407
|
+
- Deprecated `safeTry` remains a separate helper returning the final Result. It is not an identity alias of modern `Result.gen`.
|
|
408
|
+
- Its implementation belongs to the Async compatibility layer; root imports re-export the same deprecated helper. Use `ResultTask.gen` for Task workflows; Task has no `safeTry` alias.
|
|
447
409
|
|
|
448
|
-
|
|
410
|
+
---
|
|
449
411
|
|
|
450
|
-
|
|
451
|
-
| --- | --- |
|
|
452
|
-
| `runExit(task)` | Preserves `Success`, typed `Fail`, `Die`, `Interrupt`, and `Sequential` causes |
|
|
453
|
-
| `runResult(task)` | Returns a single typed failure as `Err`; rejects defects, interruption, and composite causes |
|
|
454
|
-
| `runPromise(task)` | Returns `T`; rejects every failure for integration with Promise-only APIs |
|
|
412
|
+
## Recover And Handle Results At Boundaries
|
|
455
413
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
`
|
|
414
|
+
### Local recovery inside a workflow
|
|
415
|
+
|
|
416
|
+
Use `catchTag` or `catchTags` to recover from specific tagged errors while leaving remaining error types untouched in the union:
|
|
459
417
|
|
|
460
418
|
```ts
|
|
461
|
-
|
|
462
|
-
const
|
|
463
|
-
|
|
464
|
-
|
|
419
|
+
// Recover a single tag; InvalidEmailError and DatabaseError stay in the error type
|
|
420
|
+
const account = createAccount(input).catchTag("AccountExistsError", (error) =>
|
|
421
|
+
findAccountByEmail(String(error.email)),
|
|
422
|
+
);
|
|
465
423
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
424
|
+
// Or recover multiple tags at once
|
|
425
|
+
const accountOrDraft = createAccount(input).catchTags({
|
|
426
|
+
AccountExistsError: (error) => findAccountByEmail(String(error.email)),
|
|
427
|
+
InvalidEmailError: (error) => ok({ id: "draft", email: String(error.email) }),
|
|
428
|
+
});
|
|
469
429
|
```
|
|
470
430
|
|
|
471
|
-
|
|
472
|
-
environment types:
|
|
431
|
+
Use `orElse` when recovering from the entire error channel, or `orElseSucceed` / `unwrapOr` to fall back to a plain default value.
|
|
473
432
|
|
|
474
|
-
|
|
475
|
-
const userName = loadUser('user_123')
|
|
476
|
-
.map((user) => String(user.name))
|
|
477
|
-
.andThen((name) => ResultTask.succeed(name.trim()))
|
|
478
|
-
.catchAll((error) => ResultTask.succeed(`unavailable: ${error.message}`))
|
|
479
|
-
```
|
|
433
|
+
### Exhaustive handling at application boundaries
|
|
480
434
|
|
|
481
|
-
|
|
482
|
-
causes bypass recovery as `Die(ResultTaskCauseError)` with the original tree on the error's `cause`;
|
|
483
|
-
this prevents removed error types from escaping as typed failures. The equivalent functional forms
|
|
484
|
-
are `ResultTask.map`, `ResultTask.flatMap`, and `ResultTask.catchAll`.
|
|
435
|
+
At the edge of your application (HTTP controllers, CLI commands, queue consumers), turn the `Result` into a final response:
|
|
485
436
|
|
|
486
|
-
|
|
437
|
+
- **`matchTags(onOk, handlers)`**: Requires a handler for every `_tag` in `E`. If a deeper layer adds a new error type, TypeScript flags the boundary at compile time.
|
|
438
|
+
- **`matchTagsPartial(onOk, handlers, onUnhandled)`**: Handle specific tags and route the rest to a fallback.
|
|
439
|
+
- **`match(onOk, onErr)` / `match({ ok, err })`**: Handle `Ok` and `Err` directly when individual tags don't matter.
|
|
487
440
|
|
|
488
|
-
|
|
489
|
-
const response = ResultTask.tryPromise((signal) => fetch(url, { signal }))
|
|
490
|
-
// ResultTask<Response, unknown>: synchronous throws and rejections remain typed failures.
|
|
491
|
-
```
|
|
441
|
+
> On `Result`, `match`, `matchTags`, `matchTagsPartial`, and `unwrapOr` return `Out` directly. On `ResultAsync` instances (such as `task.runResult()`), they return native `Promise<Out>` terminals; their static/direct/curried forms return composable `ResultAsync<Out, never>` values. On Task, they return lazy `ResultTask<Out, never, R>` operators; use `folded.runPromise()` for `Out`, or fold directly on `task.runResult()`.
|
|
492
442
|
|
|
493
|
-
|
|
494
|
-
specific error type; both forms receive the runtime signal and preserve interruption semantics.
|
|
443
|
+
### Strict Error-only boundaries (`StrictResult` & `StrictResultAsync`)
|
|
495
444
|
|
|
496
|
-
|
|
497
|
-
|
|
445
|
+
While `Result<T, E>` allows any error type, `StrictResult<T, E extends Error>` and
|
|
446
|
+
`StrictResultAsync<T, E extends Error>` constrain synchronous and in-flight boundaries to real
|
|
447
|
+
`Error` subclasses. New async workflow definitions return `ResultTask<User, UserError>` with a
|
|
448
|
+
concrete Error union, and `task.runResult()` returns `StrictResultAsync<User, UserError>`.
|
|
498
449
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
>()('Database')
|
|
503
|
-
|
|
504
|
-
const taskWithDatabase = ResultTask.gen(function* () {
|
|
505
|
-
const database = yield* Database
|
|
506
|
-
return yield* ResultTask.tryPromise({
|
|
507
|
-
try: () => database.findUser('user_123'),
|
|
508
|
-
catch: () => 'database-error' as const,
|
|
509
|
-
})
|
|
510
|
-
})
|
|
511
|
-
|
|
512
|
-
const resultWithDatabase = await ResultTask.runResult(taskWithDatabase, {
|
|
513
|
-
services: { Database: { findUser: async () => 'Ada' } },
|
|
514
|
-
})
|
|
515
|
-
```
|
|
450
|
+
---
|
|
451
|
+
|
|
452
|
+
## Lazy Workflows With ResultTask
|
|
516
453
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
454
|
+
`ResultTask<T, E, R>` is the preferred primitive for new async workflows. It is an
|
|
455
|
+
immutable, reusable blueprint that executes only at an explicit `run*` boundary.
|
|
456
|
+
Prefer `ok` / `err`, `mapErr`, `andThen`, and `orElse` in new code. Deprecated `succeed` / `fail`,
|
|
457
|
+
`mapError`, `flatMap`, and `catchAll` retain their original contracts, as do older retry options
|
|
458
|
+
and match handler objects; prefer `Retry.policy(...)` and `{ ok, err }` for new recipes.
|
|
459
|
+
Task has no `.then` method; `await task` leaves it unexecuted, including when `R = never`.
|
|
460
|
+
Prefer the instance runners in new recipes: `task.runResult()`, `task.runPromise()`,
|
|
461
|
+
`task.runExit()`, and `task.runFork()`. The static runners remain supported with the same behavior.
|
|
462
|
+
|
|
463
|
+
Use `ok`, `err`, `mapErr`, `andThen` and `orElse` as the Task API names. Object handlers
|
|
464
|
+
use `{ ok, err }`, and retries take a policy created with `Retry.policy(...)`.
|
|
520
465
|
|
|
521
466
|
```ts
|
|
522
|
-
|
|
467
|
+
import { ResultTask } from "resultar";
|
|
468
|
+
|
|
469
|
+
const fetchUser = (id: string) =>
|
|
470
|
+
ResultTask.tryPromise({
|
|
471
|
+
try: (signal) => fetch(`/api/users/${id}`, { signal }).then((res) => res.json()),
|
|
472
|
+
catch: (cause) => new UserLoadError({ cause, id }),
|
|
473
|
+
});
|
|
523
474
|
|
|
524
|
-
const
|
|
525
|
-
const readyWithAll = ResultTask.provideServices(taskWithDatabase, { Database: database })
|
|
475
|
+
const task = fetchUser("user_123"); // Nothing has executed yet!
|
|
526
476
|
|
|
527
|
-
|
|
528
|
-
await
|
|
477
|
+
// Execute explicitly at your application boundary (returns ResultAsync<User, UserLoadError>):
|
|
478
|
+
const result = await task.runResult();
|
|
529
479
|
```
|
|
530
480
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
(`
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
`provideServices`, `provideServiceResolver`) support data-first and curried (`.pipe`) forms.
|
|
546
|
-
A provider whose value is incompatible with the required service contract is rejected at compile
|
|
547
|
-
time, including through a reused curried provider; partial providers keep the remaining
|
|
548
|
-
requirements until they are satisfied at the boundary.
|
|
549
|
-
|
|
550
|
-
Adapters can use `ResultTask.makeScope()` to own resources across executions. Run tasks with
|
|
551
|
-
`owner.use(task)` and execute `owner.close()` after consumers finish. Failed uses roll back their
|
|
552
|
-
partial acquisitions. `ResultTask.memoize(task)` shares pending and successful executions and
|
|
553
|
-
allows retry after failure; its first execution supplies the environment and resource scope.
|
|
554
|
-
|
|
555
|
-
`ResultTask.gen` composes tasks and services linearly. On short-circuit, generator `finally` blocks
|
|
556
|
-
are closed and any yielded cleanup tasks or services are interpreted before execution completes:
|
|
481
|
+
| Instance boundary | Equivalent static boundary | Return type | Behavior |
|
|
482
|
+
| --------------------------- | --------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
|
483
|
+
| `task.runResult(options?)` | `ResultTask.runResult(task, options?)` | `ResultAsync<T, E>` | Returns an in-flight `ResultAsync` resolving typed failures as `Err`; rejects defects, interruption, and composite causes |
|
|
484
|
+
| `task.runPromise(options?)` | `ResultTask.runPromise(task, options?)` | `Promise<T>` | Resolves the success value; rejects every failure |
|
|
485
|
+
| `task.runExit(options?)` | `ResultTask.runExit(task, options?)` | `Promise<Exit<T, E>>` | Never rejects; preserves every success or failure cause as a value |
|
|
486
|
+
| `task.runFork(options?)` | `ResultTask.runFork(task, options?)` | `FiberHandle<T, E>` | Starts a root fiber immediately, with interruption, observation, and cleanup ownership |
|
|
487
|
+
|
|
488
|
+
Both forms accept the same `ResultTaskRunOptions<R>`: `services`, `signal`, `clock`, `scheduler`,
|
|
489
|
+
and `yieldEvery`. Missing or incompatible services remain compile-time errors. If `R` includes
|
|
490
|
+
pending scope release errors, `runResult`, `runExit`, and `runFork` include them in their error
|
|
491
|
+
types (`E | ScopeError<R>` internally). Every call starts a separate execution; results are not
|
|
492
|
+
memoized. Because `runResult` returns `ResultAsync<T, E>`, you can either `await task.runResult()`
|
|
493
|
+
to inspect the resolved `Result<T, E>` or chain `.map()`, `.andThen()`, `.catchTag()`, `.match()`,
|
|
494
|
+
`.unwrapOr()`, and `.unwrapOrThrow()` directly on `task.runResult()`. There is no public `task.run()` runner.
|
|
557
495
|
|
|
558
496
|
```ts
|
|
559
|
-
const
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
} finally {
|
|
563
|
-
yield* ResultTask.sync(() => logger.info('load-user finished'))
|
|
564
|
-
}
|
|
565
|
-
})
|
|
497
|
+
const task = ResultTask.sync(() => "ready");
|
|
498
|
+
const first = await task.runResult();
|
|
499
|
+
const second = await ResultTask.runResult(task); // Same contract, another execution
|
|
566
500
|
```
|
|
567
501
|
|
|
568
|
-
|
|
569
|
-
|
|
502
|
+
### Parameterized tasks and tagged recovery
|
|
503
|
+
|
|
504
|
+
Declare typed service requirements with `ResultTask.service<Contract>()('Tag')` and compose lazy workflows with `ResultTask.gen` or `ResultTask.fn`. If a required service is missing at an explicit runner, TypeScript reports a compile-time error. `map` is synchronous; compose asynchronous steps with `andThen` and `tryPromise`:
|
|
505
|
+
|
|
506
|
+
```ts
|
|
507
|
+
import { createTaggedError, ok, ResultTask } from "resultar";
|
|
508
|
+
|
|
509
|
+
class UserNotFoundError extends createTaggedError({
|
|
510
|
+
name: "UserNotFoundError",
|
|
511
|
+
message: "User $id was not found",
|
|
512
|
+
}) {}
|
|
570
513
|
|
|
571
|
-
|
|
572
|
-
|
|
514
|
+
const UsersRepo = ResultTask.service<{
|
|
515
|
+
findById(id: string): ResultTask<{ id: string; name: string }, UserNotFoundError>;
|
|
516
|
+
}>()("UsersRepo");
|
|
517
|
+
|
|
518
|
+
// ResultTask.fn wraps a generator and applies post-body operators lazily per call
|
|
519
|
+
const loadUserDisplayName = ResultTask.fn(
|
|
520
|
+
function* (id: string) {
|
|
521
|
+
const repo = yield* UsersRepo;
|
|
522
|
+
const user = yield* repo.findById(id);
|
|
523
|
+
return user.name;
|
|
524
|
+
},
|
|
525
|
+
ResultTask.catchTag("UserNotFoundError", (err) => ok(`Guest (${err.id})`)),
|
|
526
|
+
);
|
|
527
|
+
// Type: (id: string) => ResultTask<string, never, Service<'UsersRepo', ...>>
|
|
528
|
+
|
|
529
|
+
// Provide services at the boundary (or ahead of time via ResultTask.provideServices)
|
|
530
|
+
const nameResult = await loadUserDisplayName("u_42").runResult({
|
|
531
|
+
services: {
|
|
532
|
+
UsersRepo: {
|
|
533
|
+
findById: (id) => UserNotFoundError.taskErr({ id }),
|
|
534
|
+
},
|
|
535
|
+
},
|
|
536
|
+
});
|
|
537
|
+
```
|
|
573
538
|
|
|
574
539
|
### Resource scopes and application lifecycle
|
|
575
540
|
|
|
541
|
+
Use `ResultTask.acquireRelease` (or `ResultTask.acquireDisposable` for native `Disposable` / `AsyncDisposable` objects) to tie resource cleanup to a scope. Finalizers always run in reverse acquisition order (LIFO)—even when the workflow fails, throws a defect, or is interrupted:
|
|
542
|
+
|
|
576
543
|
```ts
|
|
577
|
-
const
|
|
544
|
+
const dbResource = ResultTask.acquireRelease({
|
|
578
545
|
acquire: ResultTask.tryPromise({
|
|
579
546
|
try: (signal) => connectDatabase(signal),
|
|
580
547
|
catch: (cause) => new ConnectionError({ cause }),
|
|
581
548
|
}),
|
|
582
|
-
release: (
|
|
583
|
-
|
|
584
|
-
|
|
549
|
+
release: (db, exit) =>
|
|
550
|
+
ResultTask.tryPromise({
|
|
551
|
+
try: () => db.close(exit),
|
|
552
|
+
catch: (cause) => new CloseError({ cause }),
|
|
553
|
+
}),
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
const queryProgram = ResultTask.scoped(
|
|
557
|
+
ResultTask.gen(function* () {
|
|
558
|
+
const db = yield* dbResource;
|
|
559
|
+
return yield* ResultTask.tryPromise({
|
|
560
|
+
try: (signal) => db.query("SELECT 1", signal),
|
|
561
|
+
catch: (cause) => new QueryError({ cause }),
|
|
562
|
+
});
|
|
585
563
|
}),
|
|
586
|
-
|
|
564
|
+
);
|
|
565
|
+
```
|
|
587
566
|
|
|
588
|
-
|
|
589
|
-
const database = yield* connection
|
|
590
|
-
return yield* ResultTask.tryPromise({
|
|
591
|
-
try: (signal) => database.query('SELECT 1', signal),
|
|
592
|
-
catch: (cause) => new QueryError({ cause }),
|
|
593
|
-
})
|
|
594
|
-
}))
|
|
567
|
+
`acquireRelease` tracks any release error in `R` (`ResultTaskScope<CloseError>`) so inner `orElse` calls cannot accidentally swallow a pending finalizer failure before `ResultTask.scoped` (or a `run*` boundary) closes the scope.
|
|
595
568
|
|
|
596
|
-
|
|
597
|
-
```
|
|
569
|
+
### Task concurrency and resilience
|
|
598
570
|
|
|
599
|
-
|
|
600
|
-
acquisitions register finalizers in LIFO order; every finalizer is awaited once, even after a body
|
|
601
|
-
failure, defect, or interruption. Failed acquisition does not register its release. Earlier resources
|
|
602
|
-
are still released if a later acquisition fails. Each finalizer receives the same region exit,
|
|
603
|
-
before release failures are appended. Services provided around acquisition remain available during
|
|
604
|
-
release, including services used only by the release callback.
|
|
605
|
-
|
|
606
|
-
`acquireRelease` tracks deferred errors with `ResultTaskScope<ReleaseError>` in `R`. `scoped` removes
|
|
607
|
-
that requirement and adds its errors to `E`; run boundaries do the same for the root scope. Service
|
|
608
|
-
provision and `catchAll` before scope closure cannot erase pending release failures. To recover a
|
|
609
|
-
single release failure, put `catchAll` after `scoped`.
|
|
610
|
-
|
|
611
|
-
If use succeeds but release fails, the failure is returned as `Err`. If both fail, `runExit` retains
|
|
612
|
-
them as `Sequential(useCause, releaseCause)`; subsequent release failures are appended in order.
|
|
613
|
-
Release callbacks that throw produce defects. Resources acquired during a finalizer belong to a
|
|
614
|
-
private cleanup scope, which is closed before the next finalizer starts.
|
|
615
|
-
|
|
616
|
-
Cancellation is cooperative: tasks check the signal before starting, and promise operations receive
|
|
617
|
-
it. Rejection after abort is classified as interruption. The runtime waits for in-flight operations
|
|
618
|
-
to settle so a resource acquired during cancellation can still be registered and released. Cleanup
|
|
619
|
-
uses a fresh, non-aborted signal. An operation or finalizer that never settles can therefore keep
|
|
620
|
-
the run pending; there is no preemptive cancellation, scheduler, or `Fiber` runtime in this slice.
|
|
621
|
-
|
|
622
|
-
For application composition, keep ordinary factories and existing `StrictResultAsync` use cases.
|
|
623
|
-
Use service tags only when a program needs requirements supplied at execution. Put the **whole server
|
|
624
|
-
lifetime** inside the resource scope: acquire database, session, then HTTP; wait for shutdown; drain
|
|
625
|
-
HTTP, close session, then close database. Returning a server from a completed scope would return
|
|
626
|
-
already-released resources. TypeScript does not enforce resource reference lifetimes.
|
|
627
|
-
|
|
628
|
-
See the runnable [application lifecycle example](../../examples/resultar/src/application-lifecycle.ts)
|
|
629
|
-
and its [smoke test](../../examples/resultar/scripts/lifecycle-smoke.ts). Run `pnpm run example:resultar`
|
|
630
|
-
from the workspace root. Its factory and resource contracts return `ResultTask` with database,
|
|
631
|
-
session, and HTTP error types. Drivers expose Promises only inside `ResultTask.tryPromise` adapters,
|
|
632
|
-
which receive the runtime signal without threading it through every application interface.
|
|
633
|
-
Concrete adapters must clean up partial acquisitions before returning a
|
|
634
|
-
failure and implement request draining, SSE termination, and suitable deadlines.
|
|
635
|
-
|
|
636
|
-
`ResultAsync.withResource` keeps its existing best-effort release behavior. Existing `ResultAsync`
|
|
637
|
-
instances have already started; wrapping one does not make it lazy or automatically cancelable.
|
|
638
|
-
Use `ResultTask.fromResultAsync` to run a `ResultAsync` inside a task boundary: pass a
|
|
639
|
-
`(signal) => ResultAsync` factory to defer creation until execution, or an existing instance to
|
|
640
|
-
capture an already-started execution. Use `ResultTask.toResultAsync(task)` — or the
|
|
641
|
-
`ResultAsync.fromTask(task)` alias — to start a requirement-free task immediately on the default
|
|
642
|
-
runtime and consume it as `ResultAsync`.
|
|
643
|
-
|
|
644
|
-
## Production Async Policies
|
|
645
|
-
|
|
646
|
-
Resultar models common resilience policy in the expected error channel. These helpers use lazy tasks
|
|
647
|
-
so retries can start fresh work and racing helpers can pass a cooperative abort signal.
|
|
648
|
-
|
|
649
|
-
### Timeout stays typed
|
|
571
|
+
`ResultTask` pipelines share the active `AbortSignal`, `Clock`, and resource scope across retries, timeouts, races, bounded collections, and child fibers:
|
|
650
572
|
|
|
651
573
|
```ts
|
|
652
|
-
|
|
653
|
-
name: 'AccountTimeoutError',
|
|
654
|
-
message: 'Account $accountId timed out after $timeoutMs milliseconds',
|
|
655
|
-
}) {}
|
|
574
|
+
import { Clock, Fiber, ResultTask, Retry } from "resultar";
|
|
656
575
|
|
|
657
|
-
const
|
|
658
|
-
(
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
)
|
|
576
|
+
const resilientTask = fetchUser("user_123")
|
|
577
|
+
.retry(Retry.policy({ times: 2, delayMs: 100, backoff: 2, jittered: 0.2 }))
|
|
578
|
+
.timeout(2_000, () => new UserTimeoutError());
|
|
579
|
+
|
|
580
|
+
// Deterministic testing with a virtual clock (no real timers):
|
|
581
|
+
const clock = Clock.virtual();
|
|
582
|
+
const pending = resilientTask.runResult({ clock });
|
|
583
|
+
await clock.advanceBy(2_000);
|
|
584
|
+
const outcome = await pending;
|
|
664
585
|
```
|
|
665
586
|
|
|
666
|
-
|
|
667
|
-
|
|
587
|
+
- **Collections & concurrency limits**: `ResultTask.all`, `ResultTask.forEach`, and `ResultTask.validateAll` run sequentially by default or concurrently via `{ concurrency: 8 | 'unbounded' }`. Use `ResultTask.makeSemaphore(permits)` to limit concurrency across independent tasks.
|
|
588
|
+
- **Structured child fibers**: `yield* ResultTask.forkChild(task)` starts background work attached to the current scope. Join or cancel it with `Fiber.join(fiber)`, `Fiber.await(fiber)`, or `Fiber.interrupt(fiber)`.
|
|
668
589
|
|
|
669
|
-
###
|
|
590
|
+
### Portable retry policies
|
|
670
591
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
(attempt, signal) => loadAccount(accountId, { attempt, signal }),
|
|
674
|
-
{
|
|
675
|
-
times: 2,
|
|
676
|
-
delayMs: ({ nextAttempt }) => nextAttempt * 100,
|
|
677
|
-
jittered: 0.2,
|
|
678
|
-
while: (error) => error._tag === 'DatabaseBusyError',
|
|
679
|
-
},
|
|
680
|
-
)
|
|
681
|
-
```
|
|
682
|
-
|
|
683
|
-
`times` is the number of retries after the first attempt. Use `retryOrElse` when retry exhaustion
|
|
684
|
-
should continue into an explicit fallback:
|
|
592
|
+
Use `Retry.policy` to share an immutable retry configuration between Task workflows.
|
|
593
|
+
The same policy can also be passed to retained eager Async factories:
|
|
685
594
|
|
|
686
595
|
```ts
|
|
687
|
-
|
|
688
|
-
(attempt, signal) => loadAccount(accountId, { attempt, signal }),
|
|
689
|
-
{
|
|
690
|
-
times: 2,
|
|
691
|
-
orElse: () => loadCachedAccount(accountId),
|
|
692
|
-
},
|
|
693
|
-
)
|
|
694
|
-
```
|
|
596
|
+
import { ResultTask, Retry } from "resultar";
|
|
695
597
|
|
|
696
|
-
|
|
598
|
+
const transientPolicy = Retry.policy({
|
|
599
|
+
times: 3,
|
|
600
|
+
delayMs: 100,
|
|
601
|
+
backoff: 2,
|
|
602
|
+
jittered: 0.2,
|
|
603
|
+
});
|
|
697
604
|
|
|
698
|
-
|
|
699
|
-
const
|
|
700
|
-
(signal) => loadAccountFromPrimary(accountId, { signal }),
|
|
701
|
-
(signal) => loadAccountFromReplica(accountId, { signal }),
|
|
702
|
-
)
|
|
605
|
+
const taskWithRetry = fetchUser("user_123").retry(transientPolicy);
|
|
606
|
+
const result = await taskWithRetry.runResult();
|
|
703
607
|
```
|
|
704
608
|
|
|
705
|
-
|
|
706
|
-
first completed success or failure should win, or `raceAll` for several equivalent providers.
|
|
609
|
+
### Inspect failure causes
|
|
707
610
|
|
|
708
|
-
|
|
611
|
+
When executing a task with `task.runExit()`, failures are represented as a `Cause<E>` tree (`Fail`, `Die`, `Interrupt`, `Sequential`, or `Parallel`). Use the `Cause` namespace to inspect or format them:
|
|
709
612
|
|
|
710
613
|
```ts
|
|
711
|
-
|
|
712
|
-
accountInputs,
|
|
713
|
-
(input) => createAccountAsync(input.email),
|
|
714
|
-
{ concurrency: 8, discard: true },
|
|
715
|
-
)
|
|
716
|
-
```
|
|
614
|
+
import { Cause, ResultTask } from "resultar";
|
|
717
615
|
|
|
718
|
-
|
|
719
|
-
positive concurrency number for bounded work or `"unbounded"` to start every mapped task.
|
|
616
|
+
const exit = await queryProgram.runExit();
|
|
720
617
|
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
(input) => normalizeEmail(input.email).asyncMap(async (email) => email),
|
|
727
|
-
{ concurrency: 8 },
|
|
728
|
-
)
|
|
618
|
+
if (exit._tag === "Failure") {
|
|
619
|
+
Cause.failures(exit.cause); // E[] — all expected typed errors in order
|
|
620
|
+
Cause.defects(exit.cause); // unknown[] — all unexpected Die defects
|
|
621
|
+
Cause.pretty(exit.cause); // human-readable diagnostic trace with stacks & redaction
|
|
622
|
+
}
|
|
729
623
|
```
|
|
730
624
|
|
|
731
|
-
|
|
625
|
+
> For advanced `ResultTask` details—including `interruption: 'detach'`, custom `Scheduler` instruction budgets, `provideServiceResolver`, and `catchCause` / `sandbox`—see the [Full Resultar Guide](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#lazy-tasks-and-service-requirements).
|
|
732
626
|
|
|
733
|
-
|
|
734
|
-
const account = ResultAsync.withResource({
|
|
735
|
-
acquire: (signal) => databasePool.connect({ signal }),
|
|
736
|
-
use: (connection, signal) => connection.insertAccount(input, { signal }),
|
|
737
|
-
release: (connection) => connection.close(),
|
|
738
|
-
})
|
|
739
|
-
```
|
|
627
|
+
---
|
|
740
628
|
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
629
|
+
<a id="async-work-and-resilience"></a>
|
|
630
|
+
<a id="deprecated-resultasync-compatibility"></a>
|
|
631
|
+
<a id="in-flight-resultasync--deprecated-eager-compatibility"></a>
|
|
744
632
|
|
|
745
|
-
|
|
746
|
-
[racing and timeout](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#concurrent-racing-and-timeouts),
|
|
747
|
-
[retry](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#retrying-async-work),
|
|
748
|
-
[concurrency](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#async-concurrency-mapping),
|
|
749
|
-
and [resource cleanup](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#resourceful-async-iterables).
|
|
633
|
+
## In-Flight `ResultAsync` & Deprecated Eager Compatibility
|
|
750
634
|
|
|
751
|
-
|
|
635
|
+
`ResultAsync<T, E>` (and `StrictResultAsync<T, E>`) is the official in-flight `Result`
|
|
636
|
+
(`PromiseLike<Result<T, E>>`), exported from `resultar`.
|
|
637
|
+
When you execute a `ResultTask` with `task.runResult(options?)` (or `ResultTask.runResult(task, options?)`),
|
|
638
|
+
it returns a `ResultAsync<T, E>` that can be awaited directly for `Result<T, E>` or transformed
|
|
639
|
+
and consumed with the same methods as `Result` (`map`, `mapErr`, `andThen`, `orElse`,
|
|
640
|
+
`orElseSucceed`, `catchTag`, `catchTags`, `catchReason`, `catchReasons`, `unwrapReason`,
|
|
641
|
+
`filterOrElse`, `as`, `asVoid`, `if`, `when`, `whenResult`, `unless`, `unlessResult`, `inspect`,
|
|
642
|
+
`inspectError`, `inspectBoth`, `log`, `match`, `matchTags`, `matchTagsPartial`, `unwrapOr`,
|
|
643
|
+
`unwrapOrThrow`, `toAsyncDisposable`, `[Symbol.asyncIterator]`, `pipe`).
|
|
752
644
|
|
|
753
|
-
|
|
754
|
-
|
|
645
|
+
```sh
|
|
646
|
+
pnpm add resultar
|
|
647
|
+
```
|
|
755
648
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
649
|
+
In-flight value and Promise constructors (`ResultAsync.ok`, `ResultAsync.err`, `ResultAsync.unit`,
|
|
650
|
+
`ResultAsync.fromPromise`, `ResultAsync.fromSafePromise`, `ResultAsync.try`, and top-level `okAsync`,
|
|
651
|
+
`errAsync`, `unitAsync`, `fromPromise`, `fromSafePromise`, `tryResultAsync`) remain active for
|
|
652
|
+
in-flight values, mocks, and Promise boundary adaptation. Legacy eager adapters (`fromCallback`,
|
|
653
|
+
`fromThrowableAsync`, `tryAsync`, `tryCatchAsync`, `safeTry`, `runPromise`, `ResultAsync.tryCatch`,
|
|
654
|
+
`ResultAsync.fromThrowable`, `ResultAsync.fromTask`, `ResultAsync.fromCallback`) and static
|
|
655
|
+
orchestration helpers on `ResultAsync` (`retry`, `retryOrElse`, `race`, `raceAll`, `raceFirst`,
|
|
656
|
+
`raceWith`, `timeout`, `withResource`, `forEach`, `validateAll`, `loop`, `iterate`, `firstSuccessOf`,
|
|
657
|
+
`all`, `combine`, `combineWithAllErrors`, `zip`) remain available in `resultar` as deprecated
|
|
658
|
+
compatibility APIs for existing eager contracts. Prefer `ResultTask` for new asynchronous workflows
|
|
659
|
+
and orchestration.
|
|
759
660
|
|
|
760
|
-
|
|
761
|
-
input: string,
|
|
762
|
-
): ResultAsync<Account, CreateAccountAsyncError> =>
|
|
763
|
-
Result.gen(async function* () {
|
|
764
|
-
const email = yield* normalizeEmail(input)
|
|
765
|
-
yield* ensureAccountIsAvailable(email)
|
|
661
|
+
### Wrapping external async boundaries (eager compatibility)
|
|
766
662
|
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
663
|
+
| External boundary | Helper |
|
|
664
|
+
| ------------------------------------------------------------- | ----------------------------------- |
|
|
665
|
+
| Wrap an async/promise factory (catches sync throws too) | `tryResultAsync(factory, toError)` |
|
|
666
|
+
| Adapt an already-created Promise | `fromPromise(promise, toError)` |
|
|
667
|
+
| Wrap an async function into a reusable `ResultAsync` function | `fromThrowableAsync(fn, toError)` |
|
|
668
|
+
| Adapt a callback or event subscription | `ResultAsync.fromCallback(options)` |
|
|
770
669
|
|
|
771
|
-
|
|
772
|
-
|
|
670
|
+
### Existing eager resilience policies
|
|
671
|
+
|
|
672
|
+
Existing Async timeouts, retries, races, bounded collections and best-effort release keep
|
|
673
|
+
their signatures and eager behavior. Preserve them during migration; new workflows use
|
|
674
|
+
the Task policies above. Transformation callback defects can still reject the Async Promise:
|
|
773
675
|
|
|
774
676
|
```ts
|
|
775
|
-
|
|
776
|
-
async *try() {
|
|
777
|
-
const email = yield* normalizeEmail(input)
|
|
778
|
-
return persistAccount(email)
|
|
779
|
-
},
|
|
780
|
-
catch: (cause) => new AccountServiceError({ cause, email: input }),
|
|
781
|
-
})
|
|
782
|
-
```
|
|
677
|
+
import { ResultAsync } from "resultar";
|
|
783
678
|
|
|
784
|
-
|
|
679
|
+
// 1. Timeout with cooperative AbortSignal
|
|
680
|
+
const timedAccount = ResultAsync.timeout((signal) => loadAccount(accountId, { signal }), {
|
|
681
|
+
timeoutMs: 1_500,
|
|
682
|
+
onTimeout: () => new AccountTimeoutError({ accountId, timeoutMs: 1_500 }),
|
|
683
|
+
});
|
|
785
684
|
|
|
786
|
-
|
|
685
|
+
// 2. Retry transient failures with backoff, jitter, and fallback
|
|
686
|
+
const retriedAccount = ResultAsync.retryOrElse(
|
|
687
|
+
(attempt, signal) => loadAccount(accountId, { attempt, signal }),
|
|
688
|
+
{
|
|
689
|
+
times: 2,
|
|
690
|
+
delayMs: ({ nextAttempt }) => nextAttempt * 100,
|
|
691
|
+
jittered: 0.2,
|
|
692
|
+
while: (error) => error._tag === "DatabaseBusyError",
|
|
693
|
+
orElse: () => loadCachedAccount(accountId),
|
|
694
|
+
},
|
|
695
|
+
);
|
|
787
696
|
|
|
788
|
-
|
|
697
|
+
// 3. Race redundant providers (returns first Ok, aborts losers)
|
|
698
|
+
const fastestAccount = ResultAsync.race(
|
|
699
|
+
(signal) => loadFromPrimary(accountId, { signal }),
|
|
700
|
+
(signal) => loadFromReplica(accountId, { signal }),
|
|
701
|
+
);
|
|
789
702
|
|
|
790
|
-
|
|
703
|
+
// 4. Bounded concurrency over collections
|
|
704
|
+
const imported = ResultAsync.forEach(
|
|
705
|
+
users,
|
|
706
|
+
(user) => provisionAccount(user.email),
|
|
707
|
+
{ concurrency: 8 }, // or validateAll(...) to collect all errors
|
|
708
|
+
);
|
|
791
709
|
|
|
792
|
-
|
|
793
|
-
const
|
|
794
|
-
|
|
795
|
-
(
|
|
796
|
-
)
|
|
710
|
+
// 5. Acquire-use-release bracket
|
|
711
|
+
const inserted = ResultAsync.withResource({
|
|
712
|
+
acquire: (signal) => dbPool.connect({ signal }),
|
|
713
|
+
use: (conn, signal) => conn.insertAccount(input, { signal }),
|
|
714
|
+
release: (conn) => conn.close(),
|
|
715
|
+
});
|
|
797
716
|
```
|
|
798
717
|
|
|
799
|
-
|
|
718
|
+
---
|
|
800
719
|
|
|
801
|
-
|
|
802
|
-
const account = createAccountAsync(input).catchTags({
|
|
803
|
-
AccountAlreadyExistsError: (error) => findAccountByEmail(String(error.email)),
|
|
804
|
-
InvalidEmailError: (error) =>
|
|
805
|
-
ok({ email: String(error.email), id: 'draft_account' }),
|
|
806
|
-
})
|
|
807
|
-
```
|
|
720
|
+
### Async Result generators and bridges
|
|
808
721
|
|
|
809
|
-
|
|
810
|
-
|
|
722
|
+
Core `Result.gen` / `Result.fn` support sync and async bodies. Async bodies
|
|
723
|
+
start eagerly and return `ResultAsync`:
|
|
811
724
|
|
|
812
725
|
```ts
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
726
|
+
import { Result, ResultTask, tryResultAsync } from "resultar";
|
|
727
|
+
|
|
728
|
+
const loadLegacyAccount = (id: string) =>
|
|
729
|
+
Result.gen(async function* () {
|
|
730
|
+
const account = yield* tryResultAsync(
|
|
731
|
+
() => accountClient.load(id),
|
|
732
|
+
(cause) => new AccountLoadError({ cause }),
|
|
733
|
+
);
|
|
734
|
+
return account;
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
// The factory creates a fresh eager operation on each Task run.
|
|
738
|
+
const task = ResultTask.fromResultAsync(() => loadLegacyAccount("acct_1"));
|
|
739
|
+
const result = await task.runResult();
|
|
740
|
+
|
|
741
|
+
// task.runResult() returns ResultAsync directly.
|
|
742
|
+
const inFlight = task.runResult();
|
|
743
|
+
const sameExecution = await inFlight;
|
|
816
744
|
```
|
|
817
745
|
|
|
818
|
-
|
|
819
|
-
|
|
746
|
+
Passing an existing Async to `fromResultAsync(existing)` reuses its execution on every run.
|
|
747
|
+
The factory form receives the active signal; pass it to cooperative clients when supported.
|
|
748
|
+
Provide all services and close required scopes before calling `task.runResult()`.
|
|
749
|
+
Static/instance `ResultTask.toResultAsync` and `ResultAsync.fromTask(task)` remain supported as
|
|
750
|
+
deprecated aliases of `task.runResult()`, and tagged-error `.errAsync` helpers remain supported.
|
|
751
|
+
Result `asyncMap` / `asyncAndThen` methods and statics remain supported.
|
|
820
752
|
|
|
821
|
-
|
|
753
|
+
Async instance `match`, `matchTags`, `matchTagsPartial` and `unwrapOr` remain native Promise terminals.
|
|
754
|
+
Their static/direct/curried forms return Async values for composition. At an instance boundary,
|
|
755
|
+
`await inFlight.match({ ok: account => account.id, err: () => 'unavailable' })` produces the raw
|
|
756
|
+
handler output. Async `tap` / `tapError` remain deprecated best-effort aliases of
|
|
757
|
+
`inspect` / `inspectError`; Task's fallible `tap` operations have a separate contract.
|
|
822
758
|
|
|
823
|
-
|
|
824
|
-
or UI state.
|
|
759
|
+
---
|
|
825
760
|
|
|
826
|
-
|
|
761
|
+
## Pattern Matching With Match
|
|
762
|
+
|
|
763
|
+
`Match` brings exhaustive, type-narrowing pattern matching to ordinary TypeScript unions, domain events, and HTTP route error mappers. Tag and discriminator tables compile into $O(1)$ lookup tables:
|
|
827
764
|
|
|
828
765
|
```ts
|
|
829
|
-
|
|
830
|
-
(account) => ({
|
|
831
|
-
body: account,
|
|
832
|
-
statusCode: 201,
|
|
833
|
-
}),
|
|
834
|
-
{
|
|
835
|
-
AccountAlreadyExistsError: (error) => ({
|
|
836
|
-
body: { code: error._tag, message: error.message },
|
|
837
|
-
statusCode: 409,
|
|
838
|
-
}),
|
|
839
|
-
DatabaseError: (error) => ({
|
|
840
|
-
body: { code: error._tag, message: error.message },
|
|
841
|
-
statusCode: 503,
|
|
842
|
-
}),
|
|
843
|
-
InvalidEmailError: (error) => ({
|
|
844
|
-
body: { code: error._tag, message: error.message },
|
|
845
|
-
statusCode: 400,
|
|
846
|
-
}),
|
|
847
|
-
},
|
|
848
|
-
)
|
|
849
|
-
```
|
|
766
|
+
import { Match } from "resultar";
|
|
850
767
|
|
|
851
|
-
|
|
852
|
-
`matchTagsPartial` with a fallback. Use plain `match` when the distinction between individual error
|
|
853
|
-
tags does not matter.
|
|
768
|
+
type DomainEvent = { _tag: "UserCreated"; id: string } | { _tag: "UserDeleted"; count: number };
|
|
854
769
|
|
|
855
|
-
|
|
856
|
-
|
|
770
|
+
// Build a reusable matcher function
|
|
771
|
+
const describeEvent = Match.type<DomainEvent>()
|
|
772
|
+
.tag("UserCreated", (event) => `Created ${event.id}`)
|
|
773
|
+
.tag("UserDeleted", (event) => `Deleted ${event.count} records`)
|
|
774
|
+
.exhaustive();
|
|
857
775
|
|
|
858
|
-
|
|
776
|
+
// Or match a value immediately by tag
|
|
777
|
+
const summary = Match.valueTags(event, {
|
|
778
|
+
UserCreated: (e) => e.id,
|
|
779
|
+
UserDeleted: (e) => String(e.count),
|
|
780
|
+
});
|
|
781
|
+
```
|
|
859
782
|
|
|
860
|
-
|
|
861
|
-
inside narrow local workflows.
|
|
783
|
+
### Reusable HTTP error handlers with `Match.context`
|
|
862
784
|
|
|
863
|
-
|
|
785
|
+
When multiple Hono or Fastify routes share the same error-to-HTTP mapping, extract a context-aware matcher:
|
|
864
786
|
|
|
865
787
|
```ts
|
|
866
|
-
import type {
|
|
788
|
+
import type { Context } from "hono";
|
|
789
|
+
import { Match } from "resultar";
|
|
867
790
|
|
|
868
|
-
const
|
|
869
|
-
|
|
870
|
-
|
|
791
|
+
const respondError = Match.context<
|
|
792
|
+
UserNotFoundError | DatabaseError,
|
|
793
|
+
[c: Context]
|
|
794
|
+
>().tagsExhaustive({
|
|
795
|
+
UserNotFoundError: (err, c) => c.json({ error: err.message }, 404),
|
|
796
|
+
DatabaseError: (_err, c) => c.json({ error: "Service unavailable" }, 503),
|
|
797
|
+
});
|
|
871
798
|
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
799
|
+
// Inside a route handler:
|
|
800
|
+
const result = await users.findById(id);
|
|
801
|
+
return result.match({
|
|
802
|
+
ok: (user) => c.json(user, 200),
|
|
803
|
+
err: (error) => respondError(error, c),
|
|
804
|
+
});
|
|
875
805
|
```
|
|
876
806
|
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
807
|
+
| Pattern need | `Match` API |
|
|
808
|
+
| ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
|
|
809
|
+
| Match literals, shapes, predicates, or built-in guards (`Match.string`, `Match.number`, `Match.defined`, `Match.instanceOf`, etc.) | `when`, `whenOr`, `whenAnd`, `not` |
|
|
810
|
+
| Match `_tag` unions | `tag`, `tags`, `tagsExhaustive`, `valueTags`, `typeTags` |
|
|
811
|
+
| Match custom discriminant fields or prefixes | `discriminator`, `discriminatorsExhaustive`, `tagStartsWith` |
|
|
812
|
+
| Finalize matcher | `exhaustive`, `orElse`, `result` (`Result<Matched, Remaining>`), `option` |
|
|
813
|
+
|
|
814
|
+
---
|
|
880
815
|
|
|
881
816
|
## Prevent Ignored Results
|
|
882
817
|
|
|
883
|
-
Result
|
|
818
|
+
A `Result` only protects your application if callers actually handle it. Install [`resultar-check`](https://github.com/inaiat/resultar/blob/main/packages/check/README.md)—a native TypeScript-Go CLI and LSP server that runs compiler typechecks and Resultar-specific rules in a single pass:
|
|
884
819
|
|
|
885
820
|
```sh
|
|
886
821
|
pnpm add -D resultar-check
|
|
887
822
|
```
|
|
888
823
|
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
```json
|
|
892
|
-
{
|
|
893
|
-
"scripts": {
|
|
894
|
-
"check": "resultar-check"
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
```
|
|
898
|
-
|
|
899
|
-
The CLI uses TypeScript-Go to run compiler and Resultar diagnostics over the same `tsconfig.json`.
|
|
900
|
-
Configure its rules in the project file:
|
|
824
|
+
Add the rule configuration to `tsconfig.json`:
|
|
901
825
|
|
|
902
826
|
```json
|
|
903
827
|
{
|
|
@@ -913,108 +837,94 @@ Configure its rules in the project file:
|
|
|
913
837
|
}
|
|
914
838
|
```
|
|
915
839
|
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
840
|
+
Run `resultar-check` in CI or `resultar-check lsp` in your editor to catch discarded `Result` / `ResultAsync` / `ResultTask` values, unprovided `any` task requirements, raw `throw` statements (`noThrow`), and ad-hoc `try/catch` blocks (`noTryCatch`). See the [`resultar-check` README](https://github.com/inaiat/resultar/blob/main/packages/check/README.md) for all rules and editor setup.
|
|
841
|
+
|
|
842
|
+
---
|
|
843
|
+
|
|
844
|
+
## Ecosystem Packages
|
|
919
845
|
|
|
920
|
-
|
|
921
|
-
[`resultar-check` guide](https://github.com/inaiat/resultar/blob/main/packages/check/README.md)
|
|
922
|
-
for all diagnostics, severities, modes, and ignore patterns.
|
|
846
|
+
Resultar's companion packages integrate directly with the core primitives:
|
|
923
847
|
|
|
924
|
-
|
|
848
|
+
| Package | When to use it | Documentation |
|
|
849
|
+
| ------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
|
|
850
|
+
| [`resultar-check`](https://www.npmjs.com/package/resultar-check) | Native TypeScript-Go compiler + Resultar linter & LSP | [README](https://github.com/inaiat/resultar/blob/main/packages/check/README.md) |
|
|
851
|
+
| [`resultar-request`](https://www.npmjs.com/package/resultar-request) | Fetch-first JSON HTTP client returning `ResultAsync` with retries & typed errors | [README](https://github.com/inaiat/resultar/blob/main/packages/request/README.md) |
|
|
852
|
+
| [`resultar-request-zod`](https://www.npmjs.com/package/resultar-request-zod) | Zod schema validation adapter for `resultar-request` | [README](https://github.com/inaiat/resultar/blob/main/packages/request-zod/README.md) |
|
|
853
|
+
| [`resultar-request-typebox`](https://www.npmjs.com/package/resultar-request-typebox) | TypeBox schema validation adapter for `resultar-request` | [README](https://github.com/inaiat/resultar/blob/main/packages/request-typebox/README.md) |
|
|
854
|
+
| [`resultar-di`](https://www.npmjs.com/package/resultar-di) | Typed dependency injection, class services, and scoped resources for `ResultTask` | [README](https://github.com/inaiat/resultar/blob/main/packages/di/README.md) |
|
|
855
|
+
| [`resultar-hono`](https://www.npmjs.com/package/resultar-hono) | Hono integration with per-request DI scopes and graceful shutdown | [README](https://github.com/inaiat/resultar/blob/main/packages/hono/README.md) |
|
|
856
|
+
| [`resultar-fastify`](https://www.npmjs.com/package/resultar-fastify) | Fastify plugin with application/request DI scopes and lifecycle cleanup | [README](https://github.com/inaiat/resultar/blob/main/packages/fastify/README.md) |
|
|
925
857
|
|
|
926
|
-
|
|
927
|
-
HTTP status, JSON parsing, validation, and retry failures.
|
|
858
|
+
Deprecated eager compatibility is included directly in [`resultar`](#in-flight-resultasync--deprecated-eager-compatibility); no additional package or subpath is needed.
|
|
928
859
|
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
| [`resultar-request`](https://www.npmjs.com/package/resultar-request) | You use a custom validator or decoder | [README](https://github.com/inaiat/resultar/blob/main/packages/request/README.md) |
|
|
932
|
-
| [`resultar-request-typebox`](https://www.npmjs.com/package/resultar-request-typebox) | Your response contract is a TypeBox schema | [README](https://github.com/inaiat/resultar/blob/main/packages/request-typebox/README.md) |
|
|
933
|
-
| [`resultar-request-zod`](https://www.npmjs.com/package/resultar-request-zod) | Your response contract is a Zod schema or transform | [README](https://github.com/inaiat/resultar/blob/main/packages/request-zod/README.md) |
|
|
860
|
+
Request helpers return an in-flight `ResultAsync` (`StrictResultAsync`) that starts executing on call. Their public
|
|
861
|
+
execution contract is unchanged. Wrap the call in a factory to create a new request per Task run:
|
|
934
862
|
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
863
|
+
```ts
|
|
864
|
+
import { ResultTask } from "resultar";
|
|
865
|
+
import { requestJson } from "resultar-request";
|
|
866
|
+
|
|
867
|
+
const loadAccount = (id: string) =>
|
|
868
|
+
ResultTask.fromResultAsync((signal) =>
|
|
869
|
+
requestJson({
|
|
870
|
+
request: () => fetch(`/api/accounts/${id}`, { signal }),
|
|
871
|
+
validator: (value): value is { readonly id: string } =>
|
|
872
|
+
typeof value === "object" &&
|
|
873
|
+
value !== null &&
|
|
874
|
+
"id" in value &&
|
|
875
|
+
typeof value.id === "string",
|
|
876
|
+
}),
|
|
877
|
+
);
|
|
878
|
+
|
|
879
|
+
const result = await loadAccount("acct_1").runResult();
|
|
880
|
+
```
|
|
881
|
+
|
|
882
|
+
---
|
|
938
883
|
|
|
939
884
|
## API Decision Guide
|
|
940
885
|
|
|
941
|
-
|
|
|
942
|
-
|
|
|
943
|
-
| Create success or failure
|
|
944
|
-
|
|
|
945
|
-
|
|
|
946
|
-
|
|
|
947
|
-
| Transform
|
|
948
|
-
|
|
|
949
|
-
| Recover from
|
|
950
|
-
|
|
|
951
|
-
|
|
|
952
|
-
|
|
|
953
|
-
|
|
|
954
|
-
|
|
|
955
|
-
|
|
|
956
|
-
|
|
|
957
|
-
|
|
|
958
|
-
|
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
The
|
|
970
|
-
[complete API map](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#api-map)
|
|
971
|
-
covers overloads, aliases, callback semantics, collection helpers, conditional helpers, tagged
|
|
972
|
-
reasons, disposable results, and compatibility APIs.
|
|
973
|
-
|
|
974
|
-
## Coding agents
|
|
975
|
-
|
|
976
|
-
Each npm build includes `dist/llms.txt`, `dist/agent/SKILL.md`, focused references, a compiled
|
|
977
|
-
core workflow example, and `dist/agent/versions.json`. Start from these installed files so the
|
|
978
|
-
guide matches the package version. Companion package READMEs take precedence when their installed
|
|
979
|
-
versions differ from the recorded companion versions. The online `main` guide tracks development.
|
|
980
|
-
|
|
981
|
-
The guide distinguishes `Result.gen` from `ResultTask.gen`, immediate from lazy execution,
|
|
982
|
-
observation from effectful `tap`, and `runResult` from the non-rejecting `runExit` boundary.
|
|
886
|
+
| Goal | Recommended API |
|
|
887
|
+
| ---------------------------------------------- | ------------------------------------------------------------------------------------------ |
|
|
888
|
+
| Create success or failure | Sync: `ok`, `err`, `unit`; Task: `ResultTask.ok`, `ResultTask.err` |
|
|
889
|
+
| Define a structured error class | `createTaggedError({ name, message })` (`.withProps<...>()` for custom field types) |
|
|
890
|
+
| Lift nullable values or predicates | `Result.fromNullishOr`, `Result.liftPredicate` |
|
|
891
|
+
| Wrap throwing sync or async code | Sync: `tryResult` / `fromThrowable`; async: `ResultTask.tryPromise` |
|
|
892
|
+
| Transform or chain results | `map`, `mapErr`, `andThen`, `filterOrElse`, `as`, `asVoid`; Task `map` is synchronous |
|
|
893
|
+
| Write linear generator workflows | `Result.gen`, `Result.fn`, `ResultTask.gen`, `ResultTask.fn` |
|
|
894
|
+
| Recover from specific errors | `catchTag`, `catchTags`, `orElse`, `orElseSucceed`, `unwrapOr` |
|
|
895
|
+
| Handle results at a boundary | `matchTags`, `matchTagsPartial`, `match` |
|
|
896
|
+
| Combine multiple results | Sync: `Result.all`; Task: `ResultTask.all`, `forEach`, `validateAll`, `firstSuccessOf` |
|
|
897
|
+
| Add async timeout, retry, or race | `task.timeout`, `task.retry`, `ResultTask.race`, `raceFirst`, `Retry.policy` |
|
|
898
|
+
| Build lazy, cancelable workflows | `ResultTask.tryPromise`, `sync`, `try`, `service`, `provideServices` |
|
|
899
|
+
| Manage resource lifecycles | `ResultTask.acquireRelease`, `acquireDisposable`, `scoped`, `ensuring` |
|
|
900
|
+
| Run lazy tasks at an edge | `task.runResult()`, `runExit()`, `runPromise()`, `runFork()` (static forms also supported) |
|
|
901
|
+
| Observe without altering the channel | `inspect`, `inspectError`, `inspectBoth`, `log` |
|
|
902
|
+
| Preserve an existing eager Async boundary | `tryResultAsync`, `fromPromise`, `fromThrowableAsync`, `okAsync`, `errAsync` |
|
|
903
|
+
| Adapt eager Async or request helpers into Task | `ResultTask.fromResultAsync(signal => load(signal))` |
|
|
904
|
+
|
|
905
|
+
See the [Complete API Map](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md#api-map) for all overloads, collection helpers, and compatibility aliases.
|
|
906
|
+
|
|
907
|
+
---
|
|
908
|
+
|
|
909
|
+
## Coding Agents
|
|
910
|
+
|
|
911
|
+
Every npm release bundles `dist/llms.txt`, `dist/agent/SKILL.md`, focused API and service references, and a compiled workflow example inside `node_modules/resultar/dist/agent/`. Point your coding agent to those installed files so its guidance always matches the exact installed version.
|
|
912
|
+
|
|
913
|
+
---
|
|
983
914
|
|
|
984
915
|
## More Documentation
|
|
985
916
|
|
|
986
|
-
- [
|
|
987
|
-
- [
|
|
988
|
-
- [
|
|
989
|
-
- [
|
|
990
|
-
- [Safe
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
-
|
|
995
|
-
-
|
|
996
|
-
|
|
997
|
-
Resultar is also published on [JSR as `@inaiat/resultar`](https://jsr.io/@inaiat/resultar).
|
|
998
|
-
|
|
999
|
-
## Limitations
|
|
1000
|
-
|
|
1001
|
-
ResultTask implements the RFC 0001 lazy core, generator with services, and resource scopes. The
|
|
1002
|
-
following are explicitly out of scope for this slice:
|
|
1003
|
-
|
|
1004
|
-
- No `Stream`, STM, caching, clustering, RPC, or platform layer; this is not a full Effect API port.
|
|
1005
|
-
- No implicit global runtime: every `run*` call creates its own root scope and signal, and simple
|
|
1006
|
-
tasks need no services or configuration.
|
|
1007
|
-
- No fibers or structured concurrency yet: `forkChild`/`join`/`interrupt`, task `race`/`timeout`,
|
|
1008
|
-
and uniform `all`/`forEach` over the task runtime are still Phase 3. The `Cause` model covers
|
|
1009
|
-
`Fail`, `Die`, `Interrupt`, and `Sequential`; `Parallel` is reserved for that future work. Use the
|
|
1010
|
-
`ResultAsync` race, timeout, retry, and concurrency helpers in the meantime — they keep their own
|
|
1011
|
-
eager, per-helper protocols.
|
|
1012
|
-
- No `Schedule`, schedule-based retry, or injectable clock yet (Phase 4).
|
|
1013
|
-
- No `Layer` system in the core: service graphs with their own lifecycle belong to the optional
|
|
1014
|
-
`resultar-di` package, and the core does not depend on it.
|
|
1015
|
-
- `ResultAsync` stays eager and awaitable. `fromResultAsync` on an existing instance captures an
|
|
1016
|
-
already-started execution; only the factory form defers creation.
|
|
1017
|
-
- Validated on Node.js 24+ only; other runtimes such as Bun are unvalidated.
|
|
917
|
+
- [Gradual Adoption: `Result` to `ResultTask`](https://github.com/inaiat/resultar/blob/main/docs/gradual-adoption.md)
|
|
918
|
+
- [Full Resultar Reference (`DOCUMENTATION.md`)](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md)
|
|
919
|
+
- [v4 Migration & Deprecated API Notes](https://github.com/inaiat/resultar/blob/main/docs/migrations/v4.md)
|
|
920
|
+
- [Runnable Examples (`examples/`)](https://github.com/inaiat/resultar/tree/main/examples)
|
|
921
|
+
- [Type-Safe Error Handling Article (EN)](https://github.com/inaiat/resultar/blob/main/articles/en/type-safe.md) | [Artigo em Português (PT-BR)](https://github.com/inaiat/resultar/blob/main/articles/pt/type-safe.md)
|
|
922
|
+
|
|
923
|
+
### Scope & Limitations
|
|
924
|
+
|
|
925
|
+
- **Focused scope**: Resultar is not a full Effect port—it intentionally omits `Stream`, STM, distributed clustering, and a global application runtime.
|
|
926
|
+
- **Cooperative cancellation**: Task timeouts and fiber interruptions abort the `AbortSignal` and await scope finalizers; underlying drivers must respect the signal. `ResultTask.tryPromise` can opt into `interruption: 'detach'`. Legacy Async signal-aware helpers remain cooperative, with best-effort resource release.
|
|
927
|
+
- **Deferred execution vs. already-started Promises**: Task factories defer work until an explicit `run*`. Eager async helpers and async `Result.gen` / `Result.fn` start eagerly and share one execution. Wrapping an already-created Promise cannot defer or replay it.
|
|
1018
928
|
|
|
1019
929
|
## License
|
|
1020
930
|
|