resultar 3.7.0 → 3.9.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 +23 -4
- package/dist/agent/SKILL.md +227 -0
- package/dist/agent/agents/openai.yaml +4 -0
- package/dist/agent/examples/workflow.mjs +21 -0
- package/dist/agent/examples/workflow.mts +27 -0
- package/dist/agent/references/api.md +249 -0
- package/dist/agent/references/integrations.md +264 -0
- package/dist/agent/references/patterns.md +246 -0
- package/dist/agent/references/review-checklist.md +122 -0
- package/dist/agent/references/services.md +88 -0
- package/dist/agent/versions.json +10 -0
- package/dist/index.d.ts +65 -66
- package/dist/index.js +131 -127
- package/dist/llms.txt +11 -0
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -494,13 +494,12 @@ The callback runs only when the task executes. Use `tryPromise({ try, catch })`
|
|
|
494
494
|
specific error type; both forms receive the runtime signal and preserve interruption semantics.
|
|
495
495
|
|
|
496
496
|
Workflows can request typed services with `yield*` and receive them at the boundary. Pass the service
|
|
497
|
-
type
|
|
497
|
+
type first, then let the second call infer its literal identifier:
|
|
498
498
|
|
|
499
499
|
```ts
|
|
500
500
|
const Database = ResultTask.service<
|
|
501
|
-
{ findUser: (id: string) => Promise<string> }
|
|
502
|
-
|
|
503
|
-
>('Database')
|
|
501
|
+
{ findUser: (id: string) => Promise<string> }
|
|
502
|
+
>()('Database')
|
|
504
503
|
|
|
505
504
|
const taskWithDatabase = ResultTask.gen(function* () {
|
|
506
505
|
const database = yield* Database
|
|
@@ -529,6 +528,16 @@ await ResultTask.runResult(readyWithOne)
|
|
|
529
528
|
await ResultTask.runResult(readyWithAll)
|
|
530
529
|
```
|
|
531
530
|
|
|
531
|
+
The direct form `ResultTask.service<DatabaseContract, 'Database'>('Database')` remains supported.
|
|
532
|
+
The curried form can also be yielded inline: `yield* ResultTask.service<DatabaseContract>()('Database')`.
|
|
533
|
+
Requirements belong to `R` in `ResultTask<T, E, R>`; `Result` has no service lookup API.
|
|
534
|
+
|
|
535
|
+
Every tag creation has its own symbol identity. Inline tags resolve through named environments
|
|
536
|
+
(`provideServices` or the DI module). `provideService(token, value)` supplies both an exact token binding and a named fallback.
|
|
537
|
+
Reuse the same token to select its exact binding when identifiers collide; a separately created
|
|
538
|
+
same-name tag can resolve the named fallback. Exact token bindings take precedence. For class services and explicit dependency maps, see
|
|
539
|
+
[`Service.require` and `requires` in resultar-di](../di/README.md#class-services-and-explicit-requirements).
|
|
540
|
+
|
|
532
541
|
Infrastructure such as a dependency-injection adapter can resolve service tags lazily with
|
|
533
542
|
`provideServiceResolver(task, { Logger: () => loggerTask })`. Each required identifier has a
|
|
534
543
|
typed lazy provider. Provider errors and external requirements remain in the resulting task type;
|
|
@@ -962,6 +971,16 @@ The
|
|
|
962
971
|
covers overloads, aliases, callback semantics, collection helpers, conditional helpers, tagged
|
|
963
972
|
reasons, disposable results, and compatibility APIs.
|
|
964
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.
|
|
983
|
+
|
|
965
984
|
## More Documentation
|
|
966
985
|
|
|
967
986
|
- [Full Resultar guide](https://github.com/inaiat/resultar/blob/main/DOCUMENTATION.md)
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: resultar
|
|
3
|
+
description: Build, review, migrate, test, and document TypeScript code using Resultar v3.7+, including Result, ResultAsync, ResultTask, StrictResult, createTaggedError, Result.gen/safeTry, request adapters, and resultar-check. Use when expected failures should become typed values, thrown or rejected work must be wrapped, Resultar pipelines or async policies need design, HTTP JSON needs TypeBox or Zod validation, Resultar diagnostics need configuration, or an existing Resultar codebase needs review or migration.
|
|
4
|
+
metadata:
|
|
5
|
+
tags: [typescript, error-handling]
|
|
6
|
+
domains: [foundation]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Resultar Engineering
|
|
10
|
+
|
|
11
|
+
Use Resultar to keep expected failures explicit in TypeScript without introducing an application
|
|
12
|
+
runtime. Produce code that preserves concrete success and error types from the domain boundary to
|
|
13
|
+
the final transport, job, CLI, or integration boundary.
|
|
14
|
+
|
|
15
|
+
## Start From Repository Truth
|
|
16
|
+
|
|
17
|
+
1. Inspect the consuming project's `package.json`, lockfile, Resultar version, module format, and
|
|
18
|
+
TypeScript version before proposing code.
|
|
19
|
+
2. Prefer the installed package types and local source over remembered APIs. The core package
|
|
20
|
+
ships this guide at `node_modules/resultar/dist/agent/SKILL.md`; check its `versions.json` before
|
|
21
|
+
using companion-package recipes. Online `main` documentation may describe a newer release.
|
|
22
|
+
3. In the Resultar repository, use these sources in order:
|
|
23
|
+
- `packages/resultar/src/index.ts` for public exports.
|
|
24
|
+
- `packages/resultar/README.md` and `DOCUMENTATION.md` for current semantics and examples.
|
|
25
|
+
- `packages/check/README.md` for diagnostics.
|
|
26
|
+
- `packages/request*/README.md` and `examples/request/` for request integrations.
|
|
27
|
+
4. Adapt examples when the consumer uses an older Resultar version; do not silently emit APIs
|
|
28
|
+
that are absent locally.
|
|
29
|
+
|
|
30
|
+
## Choose The Execution Model
|
|
31
|
+
|
|
32
|
+
| Contract | `Result` / `Result.gen` | `ResultAsync` | `ResultTask` |
|
|
33
|
+
| --- | --- | --- | --- |
|
|
34
|
+
| Execution | Sync values; `Result.gen` also accepts async generators | Starts immediately | Building/composing starts nothing; each `run*` executes again |
|
|
35
|
+
| Generator return | `return ok(value)`; `yield*` short-circuits failures | Compose through `Result.gen` with `yield*` | `return value`; compose tasks, services and Results with `yield*` |
|
|
36
|
+
| Error transform / recovery | `mapErr` / `orElse` | `mapErr` / `orElse` | `mapError` / `catchAll` |
|
|
37
|
+
| `tap` / `tapError` failure | Ignored observation failure | Ignored observation failure | Returned task failures join `E`; throws/rejected Promises become `Die` |
|
|
38
|
+
| Cleanup | Disposal callbacks are best-effort | `withResource` release is best-effort | `acquireRelease` + `scoped` await release and preserve failures |
|
|
39
|
+
|
|
40
|
+
Use `ResultAsync` for existing Promise-shaped use cases. Choose `ResultTask` for reusable lazy
|
|
41
|
+
work, typed service requirements, or owned resource lifetimes. Preserve existing eager/lazy
|
|
42
|
+
contracts; converting an already-started Promise or ResultAsync cannot undo its execution.
|
|
43
|
+
`ResultTask.runResult` can reject on defects, interruption, and composite causes. Use `runExit`
|
|
44
|
+
when the boundary must inspect every exit without rejection. See [API details](references/api.md).
|
|
45
|
+
For DI, Hono and Fastify, read [services and HTTP scopes](references/services.md). Use
|
|
46
|
+
`ResultTask.service<T>()(name)` or DI `Service.require<T>()(name)` for inferred identifiers. Class
|
|
47
|
+
services support optional `requires` with a factory returning ResultTask; generator-only `make`
|
|
48
|
+
remains available. Check installed versions before using these overloads.
|
|
49
|
+
|
|
50
|
+
## Apply Opinionated Defaults
|
|
51
|
+
|
|
52
|
+
- Return `Result<T, E>` for synchronous fallible work and `ResultAsync<T, E>` for asynchronous
|
|
53
|
+
fallible work.
|
|
54
|
+
- Prefer `StrictResult<T, E extends Error>` and `StrictResultAsync<T, E extends Error>` at
|
|
55
|
+
application, service, HTTP, job, queue, CLI, and integration boundaries.
|
|
56
|
+
- Return `ResultAsync` / `StrictResultAsync` directly from async services and repositories instead
|
|
57
|
+
of `Promise<T>`, `Promise<Result>` or `Promise<StrictResult>`. Map external driver failures to
|
|
58
|
+
typed errors in the repository adapter; services compose that result directly. Remove the
|
|
59
|
+
function's `async` wrapper; changing only its return annotation cannot change the runtime value.
|
|
60
|
+
- Use `preferResultAsync: "error"` with `preferResultAsyncMode: "all"` to enforce these application
|
|
61
|
+
contracts. Document local rule suppressions for native framework/driver callbacks that require
|
|
62
|
+
Promise interoperability.
|
|
63
|
+
- Prefer `createTaggedError` for expected domain and application failures that need stable identity,
|
|
64
|
+
metadata, causes, serialization, or exhaustive handling.
|
|
65
|
+
- Keep error unions specific. Let composition widen them naturally; do not collapse them to `Error`
|
|
66
|
+
before the boundary.
|
|
67
|
+
- Return `Err` for expected failures. Reserve `throw` for programmer defects, invalid library
|
|
68
|
+
arguments, or deliberate terminal/process boundaries.
|
|
69
|
+
- Wrap third-party throws and promise rejections at the edge, then keep the internal flow Resultar-
|
|
70
|
+
native.
|
|
71
|
+
- Avoid `T | Error`, sentinel `null`, broad `try/catch`, rejected expected failures, and unsafe casts
|
|
72
|
+
as substitutes for a Resultar error channel.
|
|
73
|
+
- Keep `_unsafeUnwrap()` and `_unsafeUnwrapErr()` in tests. Use `unwrapOrThrow()` only when throwing
|
|
74
|
+
is explicitly the intended boundary behavior.
|
|
75
|
+
|
|
76
|
+
## Choose The Composition Operator
|
|
77
|
+
|
|
78
|
+
| Need | Prefer |
|
|
79
|
+
| ----------------------------------------- | ------------------------ |
|
|
80
|
+
| Transform an `Ok` without failure | `map` |
|
|
81
|
+
| Transform an `Err` | `mapErr` |
|
|
82
|
+
| Validate an `Ok` with a predicate | `filterOrElse` |
|
|
83
|
+
| Continue with a sync fallible step | `andThen` |
|
|
84
|
+
| Continue from `Result` into `ResultAsync` | `asyncAndThen` |
|
|
85
|
+
| Recover the whole error channel | `orElse` |
|
|
86
|
+
| Recover selected tagged errors | `catchTag` / `catchTags` |
|
|
87
|
+
| Branch directly | `isOk()` / `isErr()` |
|
|
88
|
+
| Convert a result at a boundary | `match` / `matchTags` |
|
|
89
|
+
| Map selected tagged errors with fallback | `matchTagsPartial` |
|
|
90
|
+
| Reuse a named Result combinator | `pipe` |
|
|
91
|
+
| Write a readable linear pipeline | `safeTry` with `yield*` |
|
|
92
|
+
|
|
93
|
+
Use `catchTags` for partial pipeline recovery; unhandled tags stay in the error channel. Do not
|
|
94
|
+
invent `partialCatchTags`. Use `matchTagsPartial` only for boundary matching with a deliberate
|
|
95
|
+
fallback.
|
|
96
|
+
|
|
97
|
+
## Model Tagged Errors
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
import { createTaggedError, ok } from "resultar";
|
|
101
|
+
import type { StrictResult } from "resultar";
|
|
102
|
+
|
|
103
|
+
class InvalidEmailError extends createTaggedError({
|
|
104
|
+
name: "InvalidEmailError",
|
|
105
|
+
message: "Invalid email $email",
|
|
106
|
+
}) {}
|
|
107
|
+
|
|
108
|
+
const validateEmail = (email: string): StrictResult<string, InvalidEmailError> =>
|
|
109
|
+
email.includes("@") ? ok(email) : InvalidEmailError.err({ email });
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
- Let message-template variables define required constructor props.
|
|
113
|
+
- Preserve `cause` when converting an external failure.
|
|
114
|
+
- Treat `ErrorClass.is(value)` as nominal; do not rely on spoofed `_tag` objects.
|
|
115
|
+
- Use `TaggedEnum` only for lightweight tagged domain variants that do not need to be real errors.
|
|
116
|
+
- Include an `Error` handler when a matched union can contain untagged errors.
|
|
117
|
+
|
|
118
|
+
## Compose Async Work
|
|
119
|
+
|
|
120
|
+
Wrap uncontrolled async work with the helper that matches how it is created:
|
|
121
|
+
|
|
122
|
+
- Use `fromPromise(promise, toError)` for an already-created promise.
|
|
123
|
+
- Use `tryResultAsync({ try, catch })` or `tryResultAsync(factory, toError)` when creating the
|
|
124
|
+
promise can throw synchronously.
|
|
125
|
+
- Use `fromSafePromise` only when rejection is impossible by contract.
|
|
126
|
+
- Use `fromCallback` for callback or subscription APIs with explicit cleanup.
|
|
127
|
+
- Use `runPromise` only at a terminal boundary that intentionally resolves or throws.
|
|
128
|
+
|
|
129
|
+
Prefer lazy Resultar policies over ad hoc timers or `Promise.race`:
|
|
130
|
+
|
|
131
|
+
- Use `ResultAsync.retry` / `retryOrElse` for typed retry and fallback policy.
|
|
132
|
+
- Use `timeout`, `race`, `raceAll`, or `raceFirst` for signal-aware concurrency.
|
|
133
|
+
- Use mapped `forEach` for first-error batch work and mapped `validateAll` for collecting every
|
|
134
|
+
independent validation error.
|
|
135
|
+
- Use `withResource` when successful acquisition must always trigger best-effort release.
|
|
136
|
+
- Pass the provided abort signal into the underlying client; cancellation is cooperative.
|
|
137
|
+
|
|
138
|
+
On `Result` and `ResultAsync`, use `tap`, `tapError`, and `log` for best-effort observation.
|
|
139
|
+
`ResultTask.tap` and `tapError` participate in failure handling; they do not swallow failures. Use `andThen`, `orElse`, or a
|
|
140
|
+
Resultar wrapper when callback failure must change control flow.
|
|
141
|
+
|
|
142
|
+
## Use `Result.gen` For Linear Result Flows
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
const createUser = (input: CreateUserInput): StrictResultAsync<User, CreateUserError> =>
|
|
146
|
+
Result.gen(async function* () {
|
|
147
|
+
const email = yield* validateEmail(input.email);
|
|
148
|
+
const available = yield* ensureEmailAvailable(email);
|
|
149
|
+
const user = yield* insertUser({ ...input, email: available });
|
|
150
|
+
|
|
151
|
+
return ok(user);
|
|
152
|
+
});
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
- `safeTry` is the top-level alias of `Result.gen`; both have the same rules.
|
|
156
|
+
- Use `yield*` for both `Result` and `ResultAsync` values.
|
|
157
|
+
- Wrap raw promises before yielding them.
|
|
158
|
+
- Do not use raw `await` or broad `try/catch` inside a Resultar `safeTry` workflow.
|
|
159
|
+
- Prefer `yield* result` over legacy `safeUnwrap()`.
|
|
160
|
+
|
|
161
|
+
## Keep Boundaries Concrete
|
|
162
|
+
|
|
163
|
+
- Define request, command, and event payloads with concrete types; do not widen them to generic JSON
|
|
164
|
+
records before the use case.
|
|
165
|
+
- Keep transport concerns out of domain code. Convert tagged errors to HTTP responses, CLI exit
|
|
166
|
+
codes, job outcomes, or logs at the outer boundary.
|
|
167
|
+
- Use exhaustive `matchTags` when every tagged error needs a distinct mapping.
|
|
168
|
+
- Use local `catchTag` / `catchTags` only when recovery belongs to domain policy.
|
|
169
|
+
|
|
170
|
+
## Handle HTTP JSON Requests
|
|
171
|
+
|
|
172
|
+
Use the Resultar request family instead of repeating fetch, parsing, validation, retry, and error
|
|
173
|
+
mapping logic:
|
|
174
|
+
|
|
175
|
+
- Use `resultar-request` with `validator` or `decode` for schema-library-agnostic boundaries.
|
|
176
|
+
- Use `resultar-request-typebox` when contracts use TypeBox and preserve `Static<typeof schema>`.
|
|
177
|
+
- Use `resultar-request-zod` when contracts use Zod and preserve transformed
|
|
178
|
+
`z.output<typeof schema>`.
|
|
179
|
+
- Map request, HTTP, invalid-JSON, and validation failures to concrete tagged errors when they cross
|
|
180
|
+
an application boundary.
|
|
181
|
+
|
|
182
|
+
Read `references/integrations.md` before implementing request adapters or diagnostics.
|
|
183
|
+
|
|
184
|
+
## Validate In The Preferred Order
|
|
185
|
+
|
|
186
|
+
1. Run the `resultar-check` CLI as the authoritative project and CI gate. It runs TypeScript and the
|
|
187
|
+
full Resultar rule set against the project.
|
|
188
|
+
2. Configure `resultar-check lsp` as the editor's stdio language server for inline diagnostics and
|
|
189
|
+
quick fixes. It reads the same project configuration as the CLI.
|
|
190
|
+
3. Use JSONL, SARIF, or JUnit output when another CI or review system needs structured diagnostics;
|
|
191
|
+
do not present those consumers as replacements for the native checker.
|
|
192
|
+
4. Run the repository's native formatting, build, tests, analysis, package smoke, and example
|
|
193
|
+
workflows when available.
|
|
194
|
+
|
|
195
|
+
JSONL fixes distinguish `correction` from `intentional-discard`; LSP actions carry the same
|
|
196
|
+
classification in `data.kind`. Do not apply a discard just to silence the checker: `void task`
|
|
197
|
+
does not execute a lazy task or handle its errors. Corrections still need compilation and tests.
|
|
198
|
+
|
|
199
|
+
Treat stable `resultar/*` rule IDs as architecture feedback. Fix the typed boundary or composition
|
|
200
|
+
problem instead of suppressing diagnostics broadly. Use narrow ignore patterns only for deliberate
|
|
201
|
+
test, script, generated, or process-boundary exceptions.
|
|
202
|
+
|
|
203
|
+
## Test And Document Behavior
|
|
204
|
+
|
|
205
|
+
- Cover `Ok` and every expected `Err` path with runtime tests.
|
|
206
|
+
- Cover inferred error unions, tagged-error props, narrowing, and exhaustive handlers with type
|
|
207
|
+
tests. Use `expectTypeOf` and `@ts-expect-error` where supported.
|
|
208
|
+
- Use the consuming project's test runner. Use `vite-plus/test` inside this repository.
|
|
209
|
+
- Test retry, timeout, cancellation, concurrency, and cleanup policies with deterministic fakes.
|
|
210
|
+
- Derive documentation examples from checked source or runnable examples and keep package links
|
|
211
|
+
reciprocal when packages are alternatives or adapters.
|
|
212
|
+
|
|
213
|
+
## Review Before Finishing
|
|
214
|
+
|
|
215
|
+
- Run the project's Resultar CLI check after edits.
|
|
216
|
+
- Confirm no Result, ResultAsync, or ResultTask value is silently discarded.
|
|
217
|
+
- Confirm mapped errors preserve `cause` and useful metadata.
|
|
218
|
+
- Confirm boundary handlers cover the actual error union.
|
|
219
|
+
- Confirm request validation derives the downstream type instead of widening it.
|
|
220
|
+
- Confirm validation claims match commands that actually passed.
|
|
221
|
+
|
|
222
|
+
## Load References Selectively
|
|
223
|
+
|
|
224
|
+
- Read `references/api.md` for current exports, collection helpers, and async policy semantics.
|
|
225
|
+
- Read `references/patterns.md` for copyable implementation and migration patterns.
|
|
226
|
+
- Read `references/integrations.md` for request adapters and the CLI/editor/lint workflow.
|
|
227
|
+
- Read `references/review-checklist.md` for reviews, migrations, and final validation.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createTaggedError, ok, Result, ResultTask } from 'resultar';
|
|
2
|
+
export class InvalidEmailError extends createTaggedError({
|
|
3
|
+
name: 'InvalidEmailError',
|
|
4
|
+
message: 'Invalid email $email',
|
|
5
|
+
}) {
|
|
6
|
+
}
|
|
7
|
+
export const validateEmail = (email) => {
|
|
8
|
+
const normalized = email.trim().toLowerCase();
|
|
9
|
+
return normalized.includes('@') ? ok(normalized) : InvalidEmailError.err({ email });
|
|
10
|
+
};
|
|
11
|
+
export const account = (email) => Result.gen(function* () {
|
|
12
|
+
const normalized = yield* validateEmail(email);
|
|
13
|
+
return ok({ email: normalized });
|
|
14
|
+
});
|
|
15
|
+
export const Greeting = ResultTask.service('greeting');
|
|
16
|
+
// Creating this task runs no work. The boundary supplies its typed dependency.
|
|
17
|
+
export const welcome = (email) => ResultTask.gen(function* () {
|
|
18
|
+
const normalized = yield* validateEmail(email);
|
|
19
|
+
const greeting = yield* Greeting;
|
|
20
|
+
return `${greeting}, ${normalized}`;
|
|
21
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createTaggedError, ok, Result, ResultTask } from 'resultar'
|
|
2
|
+
import type { StrictResult } from 'resultar'
|
|
3
|
+
|
|
4
|
+
export class InvalidEmailError extends createTaggedError({
|
|
5
|
+
name: 'InvalidEmailError',
|
|
6
|
+
message: 'Invalid email $email',
|
|
7
|
+
}) {}
|
|
8
|
+
|
|
9
|
+
export const validateEmail = (email: string): StrictResult<string, InvalidEmailError> => {
|
|
10
|
+
const normalized = email.trim().toLowerCase()
|
|
11
|
+
return normalized.includes('@') ? ok(normalized) : InvalidEmailError.err({ email })
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const account = (email: string): StrictResult<{ email: string }, InvalidEmailError> =>
|
|
15
|
+
Result.gen(function* () {
|
|
16
|
+
const normalized = yield* validateEmail(email)
|
|
17
|
+
return ok({ email: normalized })
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
export const Greeting = ResultTask.service<string, 'greeting'>('greeting')
|
|
21
|
+
|
|
22
|
+
// Creating this task runs no work. The boundary supplies its typed dependency.
|
|
23
|
+
export const welcome = (email: string) => ResultTask.gen(function* () {
|
|
24
|
+
const normalized = yield* validateEmail(email)
|
|
25
|
+
const greeting = yield* Greeting
|
|
26
|
+
return `${greeting}, ${normalized}`
|
|
27
|
+
})
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# Resultar v3.7+ API Guide
|
|
2
|
+
|
|
3
|
+
Use this reference after checking the version installed by the consuming project. The local package
|
|
4
|
+
types and source are authoritative when they differ from this guide.
|
|
5
|
+
|
|
6
|
+
## Contents
|
|
7
|
+
|
|
8
|
+
- [Package Baseline](#package-baseline)
|
|
9
|
+
- [Core Types And Constructors](#core-types-and-constructors)
|
|
10
|
+
- [Composition And Recovery](#composition-and-recovery)
|
|
11
|
+
- [Tagged Errors](#tagged-errors)
|
|
12
|
+
- [Boundary Matching](#boundary-matching)
|
|
13
|
+
- [Async Boundaries](#async-boundaries)
|
|
14
|
+
- [Async Policies](#async-policies)
|
|
15
|
+
- [Collections And Control Flow](#collections-and-control-flow)
|
|
16
|
+
- [`safeTry`](#safetry)
|
|
17
|
+
- [ResultTask (Lazy Workflows)](#resulttask-lazy-workflows)
|
|
18
|
+
- [Observation And Cleanup](#observation-and-cleanup)
|
|
19
|
+
|
|
20
|
+
## Package Baseline
|
|
21
|
+
|
|
22
|
+
Resultar v3.7 is an ESM package targeting Node.js 24+. The repository currently uses TypeScript 7.
|
|
23
|
+
Do not infer consumer requirements from memory: inspect `package.json`, the lockfile, and exported
|
|
24
|
+
types first.
|
|
25
|
+
|
|
26
|
+
Prefer named imports and type-only imports:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { ResultAsync, createTaggedError, ok, safeTry, tryResult, tryResultAsync } from "resultar";
|
|
30
|
+
import type { StrictResult, StrictResultAsync } from "resultar";
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`tryCatch` and `tryCatchAsync` remain compatibility aliases. Prefer `tryResult` and
|
|
34
|
+
`tryResultAsync` in new v3.7+ code.
|
|
35
|
+
|
|
36
|
+
## Core Types And Constructors
|
|
37
|
+
|
|
38
|
+
| Need | API |
|
|
39
|
+
| --------------------------------------------------- | ---------------------------------------- |
|
|
40
|
+
| Synchronous fallible value | `Result<T, E>` |
|
|
41
|
+
| Asynchronous fallible value | `ResultAsync<T, E>` |
|
|
42
|
+
| Error-only sync channel | `StrictResult<T, E extends Error>` |
|
|
43
|
+
| Error-only async channel | `StrictResultAsync<T, E extends Error>` |
|
|
44
|
+
| Successful value | `ok(value)` / `okAsync(value)` |
|
|
45
|
+
| Successful `undefined` | `unit()` / `unitAsync()` |
|
|
46
|
+
| Expected failure | `err(error)` / `errAsync(error)` |
|
|
47
|
+
| Catch synchronous throws now | `tryResult(fn, toError)` |
|
|
48
|
+
| Wrap a synchronous throwing function | `fromThrowable(fn, toError)` |
|
|
49
|
+
| Catch promise rejection or a throwing async factory | `tryResultAsync(input, toError)` |
|
|
50
|
+
| Wrap an async throwing function | `fromThrowableAsync(fn, toError)` |
|
|
51
|
+
| Deliberately leave Resultar at a terminal boundary | `runSync(result)` / `runPromise(result)` |
|
|
52
|
+
|
|
53
|
+
Use the named options form when it improves readability:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
const parsed = tryResult({
|
|
57
|
+
try: () => JSON.parse(input) as unknown,
|
|
58
|
+
catch: (cause) => new ParsePayloadError({ cause }),
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`runSync` returns the `Ok` value or throws the `Err`; `runPromise` resolves the `Ok` value or
|
|
63
|
+
rejects with the `Err`. Keep both at framework, bootstrap, CLI, or test boundaries.
|
|
64
|
+
|
|
65
|
+
## Composition And Recovery
|
|
66
|
+
|
|
67
|
+
- `map` transforms an `Ok` value with an infallible function.
|
|
68
|
+
- `mapErr` transforms an `Err` value.
|
|
69
|
+
- `filterOrElse` validates or narrows an `Ok` value and introduces a typed error on failure.
|
|
70
|
+
- `andThen` chains fallible work. On `ResultAsync`, it accepts sync or async Resultar work.
|
|
71
|
+
- `asyncAndThen` bridges a synchronous `Result` into `ResultAsync`.
|
|
72
|
+
- `orElse` recovers the complete error channel.
|
|
73
|
+
- `catchTag` recovers one tagged error.
|
|
74
|
+
- `catchTags` recovers selected tagged errors; unhandled variants stay in the error union.
|
|
75
|
+
- `pipe` applies named combinators without changing Result semantics.
|
|
76
|
+
|
|
77
|
+
Let each step expose its narrow error type and let composition infer the union:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
type CreateUserError = InvalidEmailError | UserExistsError | SaveUserError;
|
|
81
|
+
|
|
82
|
+
const createUser = (input: CreateUserInput): StrictResultAsync<User, CreateUserError> =>
|
|
83
|
+
validateEmail(input.email)
|
|
84
|
+
.asyncAndThen(ensureEmailAvailable)
|
|
85
|
+
.andThen((email) => saveUser({ ...input, email }));
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Do not manually cast the final error union. A surprising inferred union is usually evidence that a
|
|
89
|
+
step has a broad or inaccurate signature.
|
|
90
|
+
|
|
91
|
+
## Tagged Errors
|
|
92
|
+
|
|
93
|
+
`createTaggedError` creates nominal `Error` subclasses with stable `_tag`, message-template props,
|
|
94
|
+
`cause`, `fingerprint`, `toJSON()`, and `.err()` support.
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
class UserNotFoundError extends createTaggedError({
|
|
98
|
+
name: "UserNotFoundError",
|
|
99
|
+
message: "User $userId was not found in $source",
|
|
100
|
+
}) {}
|
|
101
|
+
|
|
102
|
+
const missing = UserNotFoundError.err({ userId, source: "database" });
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
- Let `$variables` in the message template define required constructor props.
|
|
106
|
+
- Preserve the original failure with `cause` when adapting external code.
|
|
107
|
+
- Keep the configured `name` equal to the class name.
|
|
108
|
+
- Do not override the generated constructor.
|
|
109
|
+
- Treat `ErrorClass.is(value)` as a nominal `instanceof`-based guard; a matching `_tag` is not
|
|
110
|
+
sufficient.
|
|
111
|
+
- Avoid reserved template fields such as `_tag`, `name`, `message`, `messageTemplate`,
|
|
112
|
+
`fingerprint`, `stack`, and `cause`.
|
|
113
|
+
- Use `TaggedEnum`/`taggedEnum` for lightweight tagged data that should not be an `Error`.
|
|
114
|
+
- Use `redact`, `isRedacted`, and `revealRedacted` when sensitive error metadata must not serialize
|
|
115
|
+
or log by default.
|
|
116
|
+
- Use `findCause` or an instance's `findCause` to inspect a typed cause chain.
|
|
117
|
+
|
|
118
|
+
## Boundary Matching
|
|
119
|
+
|
|
120
|
+
Use branch methods inside an algorithm and matching when converting the completed result:
|
|
121
|
+
|
|
122
|
+
- `isOk()` / `isErr()` narrow for direct control flow.
|
|
123
|
+
- `match(onOk, onErr)` handles a simple boundary.
|
|
124
|
+
- `matchTags(onOk, handlers)` exhaustively maps tagged errors.
|
|
125
|
+
- `matchTagsPartial(onOk, handlers, fallback)` handles selected tags with a deliberate fallback.
|
|
126
|
+
- `matchError(error, handlers)` handles an error value directly.
|
|
127
|
+
- `matchErrorPartial(error, handlers, fallback)` partially handles an error value.
|
|
128
|
+
|
|
129
|
+
Include an `Error` handler whenever an error union can contain an untagged `Error`. Do not invent
|
|
130
|
+
`partialCatchTags`; `catchTags` already supports partial recovery.
|
|
131
|
+
|
|
132
|
+
## Async Boundaries
|
|
133
|
+
|
|
134
|
+
Choose the wrapper based on ownership of the promise:
|
|
135
|
+
|
|
136
|
+
- `fromPromise(existingPromise, toError)` wraps a promise that has already been created.
|
|
137
|
+
- `tryResultAsync(() => createPromise(), toError)` also catches synchronous factory throws and is
|
|
138
|
+
preferred when the agent controls creation.
|
|
139
|
+
- `fromSafePromise(promise)` is only for promises guaranteed not to reject by contract.
|
|
140
|
+
- `fromCallback(options)` adapts callback or subscription APIs and returns
|
|
141
|
+
`ResultAsync<T, E | AbortError>`. Supply cleanup and forward cancellation where supported.
|
|
142
|
+
- `AbortError` and `isAbortError` identify Resultar cancellation.
|
|
143
|
+
|
|
144
|
+
Always map `unknown` causes into a specific error near the external boundary:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
const loadUser = (id: string): StrictResultAsync<User, LoadUserError> =>
|
|
148
|
+
tryResultAsync(
|
|
149
|
+
() => client.loadUser(id),
|
|
150
|
+
(cause) => new LoadUserError({ cause, id }),
|
|
151
|
+
);
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Async Policies
|
|
155
|
+
|
|
156
|
+
Use the built-in lazy policies instead of hand-written `Promise.race`, timers, or retry loops:
|
|
157
|
+
|
|
158
|
+
- `ResultAsync.retry(task, options)` retries typed failures and includes `AbortError` in the error
|
|
159
|
+
channel. Use `times`, `delayMs`, `jittered`, `while`, and the provided signal deliberately.
|
|
160
|
+
- `ResultAsync.retryOrElse(task, options)` applies a typed fallback after retry exhaustion. The
|
|
161
|
+
exhausted task error is replaced by the fallback error channel.
|
|
162
|
+
- `ResultAsync.timeout(task, options)` returns the task error or the typed `onTimeout` error.
|
|
163
|
+
- `ResultAsync.race(left, right)` and `raceAll(tasks)` return the first settled result.
|
|
164
|
+
- `ResultAsync.raceFirst(left, right)` returns the first `Ok`; if both fail, it returns the last
|
|
165
|
+
observed `Err`.
|
|
166
|
+
- `ResultAsync.raceWith(left, right, handlers)` exposes the winner and a handle to the still-running
|
|
167
|
+
task for custom cooperative policy.
|
|
168
|
+
- `ResultAsync.withResource({ acquire, use, release })` releases after every acquire-success path.
|
|
169
|
+
Release errors are best-effort and do not enter the result channel; model cleanup in `use` when it
|
|
170
|
+
must affect control flow.
|
|
171
|
+
|
|
172
|
+
Race, timeout, retry, callback, and resource helpers rely on cooperative cancellation. Pass their
|
|
173
|
+
`AbortSignal` into fetch, database, SDK, timer, or subscription work; aborting a wrapper cannot stop
|
|
174
|
+
an underlying operation that ignores the signal.
|
|
175
|
+
|
|
176
|
+
## Collections And Control Flow
|
|
177
|
+
|
|
178
|
+
Both `Result` and `ResultAsync` provide collection helpers:
|
|
179
|
+
|
|
180
|
+
- `combine` preserves array/tuple/record shape and returns the first error.
|
|
181
|
+
- `combineWithAllErrors` collects every error.
|
|
182
|
+
- `validateAll` collects all validation errors; async mapped use supports bounded `concurrency`.
|
|
183
|
+
- `zip` combines exactly two values.
|
|
184
|
+
- `firstSuccessOf` tries candidates until one succeeds and otherwise returns the final error.
|
|
185
|
+
- `forEach` stops at the first error; use `{ discard: true }` when successful values are irrelevant.
|
|
186
|
+
- `when`, `unless`, `loop`, and `iterate` model Resultar-native control flow where they improve
|
|
187
|
+
clarity.
|
|
188
|
+
|
|
189
|
+
Prefer `ResultAsync.forEach(items, mapper, { concurrency })` for bounded first-error processing and
|
|
190
|
+
`ResultAsync.validateAll(items, mapper, { concurrency })` for independent validation where every
|
|
191
|
+
error matters.
|
|
192
|
+
|
|
193
|
+
## `safeTry`
|
|
194
|
+
|
|
195
|
+
Use `safeTry` when a linear generator is clearer than a chain:
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
const workflow = safeTry(async function* () {
|
|
199
|
+
const input = yield* validateInput(raw);
|
|
200
|
+
const remote = yield* loadRemote(input);
|
|
201
|
+
const saved = yield* saveRemote(remote);
|
|
202
|
+
|
|
203
|
+
return ok(saved);
|
|
204
|
+
});
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Use `yield*` for `Result` and `ResultAsync`. Wrap raw promises before yielding. Avoid raw `await`,
|
|
208
|
+
`try/catch`, and legacy `safeUnwrap()` inside the generator.
|
|
209
|
+
|
|
210
|
+
## ResultTask (Lazy Workflows)
|
|
211
|
+
|
|
212
|
+
`ResultTask<A, E, R>` is a lazy, reusable description of work: building one starts nothing, and
|
|
213
|
+
each `run*` call executes it again. Never substitute `ResultAsync` (already-started) where
|
|
214
|
+
laziness matters.
|
|
215
|
+
|
|
216
|
+
- Build: `succeed`, `fail`, `fromResult`, `sync`, `try`, `tryPromise` (`try`/`tryPromise` map
|
|
217
|
+
external throws into `E`; `sync` treats them as defects).
|
|
218
|
+
- Compose lazily: `map`, `flatMap`, `catchAll`, `tap`, `gen` with `yield*` (also accepts
|
|
219
|
+
`yield* result`: `Ok` unwraps, `Err` short-circuits into `E`).
|
|
220
|
+
- Provide services: `yield* Tag`, then `provideService` / `provideServices` /
|
|
221
|
+
`provideServiceResolver` (data-first and curried); missing or incompatible providers are
|
|
222
|
+
compile-time errors, never runtime surprises.
|
|
223
|
+
- Resources: `acquireRelease` + `scoped`; LIFO awaited finalizers run on every exit; deferred
|
|
224
|
+
release errors live in `R` until `scoped` folds them into `E`; `catchAll` recovers a release
|
|
225
|
+
failure only after `scoped`.
|
|
226
|
+
- Boundaries: `runExit` preserves `Success`/`Fail`/`Die`/`Interrupt`/`Sequential`; `runResult`
|
|
227
|
+
returns one `Err` and rejects the rest (`AbortError` for interrupts, `ResultTaskCauseError`
|
|
228
|
+
for composites); `runPromise` unwraps or rejects. Cancellation is cooperative: no fibers,
|
|
229
|
+
`race`, `timeout`, or scheduler exist on `ResultTask` yet — use the `ResultAsync` helpers.
|
|
230
|
+
|
|
231
|
+
`Result.gen` is the canonical name for linear Result workflows; `safeTry` remains as its alias.
|
|
232
|
+
For lazy `ResultTask` workflows with typed services and scopes, see `packages/resultar/README.md`
|
|
233
|
+
(Lazy Workflows With ResultTask) and `docs/rfcs/rfc-0001-result-task-core.md`.
|
|
234
|
+
|
|
235
|
+
## Observation And Cleanup
|
|
236
|
+
|
|
237
|
+
On `Result` and `ResultAsync`, `tap`, `tapError`, and `log` preserve the original result. Callback throws and rejected callback
|
|
238
|
+
promises are intentionally ignored, so use them only for best-effort logging, metrics, tracing, and
|
|
239
|
+
observation. Use `andThen`, `orElse`, or a Resultar boundary when side-effect failure must alter the
|
|
240
|
+
workflow.
|
|
241
|
+
|
|
242
|
+
`toDisposable` and `toAsyncDisposable` integrate with Node.js `using` / `await using`; their
|
|
243
|
+
cleanup callbacks are also best-effort. Prefer `withResource` for acquire/use/release workflows.
|
|
244
|
+
|
|
245
|
+
On `ResultTask`, `tap` and `tapError` are lazy effectful composition. Returned task errors and
|
|
246
|
+
requirements join the workflow type. A thrown callback or rejected Promise becomes `Die`;
|
|
247
|
+
a returned `ResultTask.fail(error)` becomes a typed failure. Use an explicitly recovered task
|
|
248
|
+
when observation must be best-effort. `runExit` preserves these exits; `runResult` can reject.
|
|
249
|
+
Do not substitute `ResultAsync.withResource` for awaited ResultTask resource ownership.
|