resultar 3.0.0 → 3.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +343 -10
- package/dist/index.d.ts +152 -3
- package/dist/index.js +567 -29
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,9 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
Production-facing error handling for TypeScript, without a runtime.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
Full documentation, long-form recipes, and the complete API map live in
|
|
6
|
+
[DOCUMENTATION.md](../../DOCUMENTATION.md).
|
|
7
|
+
|
|
8
|
+
## New In 3.1
|
|
9
|
+
|
|
10
|
+
Resultar 3.1 turns common production async policies into typed values instead of rejected promises,
|
|
11
|
+
hidden timers, or helper code that throws.
|
|
12
|
+
|
|
13
|
+
- `ResultAsync.timeout`, `retry`, `retryOrElse`, `race`, `raceAll`, and `withResource` cover
|
|
14
|
+
timeouts, retry policy, fallback, replica reads, and cleanup while preserving the expected error
|
|
15
|
+
type.
|
|
16
|
+
- `ResultAsync.forEach` and `ResultAsync.validateAll` support sequential work, bounded
|
|
17
|
+
`{ concurrency }`, and explicit unbounded execution for batch processing and validation.
|
|
18
|
+
- `AbortError` / `isAbortError` and redacted tagged-error props with `redact`, `isRedacted`, and
|
|
19
|
+
`revealRedacted` make cancellation and sensitive error metadata visible at the type boundary.
|
|
20
|
+
|
|
21
|
+
## Main Functionalities
|
|
22
|
+
|
|
23
|
+
Resultar gives you typed `Result` primitives, real `Error`-based tagged failures, async control
|
|
24
|
+
helpers, sync/async composition, and optional no-discard diagnostics. It is small enough to adopt in
|
|
25
|
+
one module and strict enough to use at service, HTTP, job, queue, CLI, and integration boundaries.
|
|
8
26
|
|
|
9
27
|
```ts
|
|
10
28
|
Result<T, E>
|
|
@@ -18,6 +36,8 @@ Use it when expected failures should be impossible to miss:
|
|
|
18
36
|
- Function signatures show both the success type and the expected error type.
|
|
19
37
|
- Domain errors are real `Error` instances with stable tags, messages, `cause`, stack traces, and
|
|
20
38
|
JSON output.
|
|
39
|
+
- `ResultAsync` can bound concurrent work, race replicas, apply typed timeouts, retry transient
|
|
40
|
+
failures, and pair acquisition with cleanup without adding a runtime scheduler.
|
|
21
41
|
- Sync and async fallible work compose without `try/catch` blocks scattered through application
|
|
22
42
|
code.
|
|
23
43
|
- Ignored `Result` values can be reported by a type-aware no-discard check.
|
|
@@ -96,6 +116,9 @@ in normal TypeScript.
|
|
|
96
116
|
| Expected failures are hidden behind `throw` | `Result<T, E>` and `ResultAsync<T, E>` in function signatures |
|
|
97
117
|
| Production errors need stable names and metadata | `createTaggedError` classes with `_tag`, template props, `cause`, and `.toJSON()` |
|
|
98
118
|
| Async flows turn into nested `try/catch` | `tryResultAsync`, `andThen`, `asyncAndThen`, `map`, `orElse`, `safeTry` |
|
|
119
|
+
| Network calls need timeouts, retries, and fallback policy | `ResultAsync.timeout`, `retry`, `retryOrElse`, `race`, and `raceAll` |
|
|
120
|
+
| Batch work needs backpressure without a framework | `ResultAsync.forEach` and mapped `validateAll` with `{ concurrency }` |
|
|
121
|
+
| Resourceful work needs cleanup on every path | `ResultAsync.withResource` and native `AsyncIterable<Result<T, E>>` recipes |
|
|
99
122
|
| Boundary handlers miss cases | `matchTags` for exhaustive tagged-error handling |
|
|
100
123
|
| Results are easy to ignore | `resultar-lint check` and the `resultar-lint` TypeScript plugin |
|
|
101
124
|
| You do not want another application runtime | A small ESM library around explicit values |
|
|
@@ -133,6 +156,62 @@ const getPosts = (
|
|
|
133
156
|
)
|
|
134
157
|
```
|
|
135
158
|
|
|
159
|
+
## Async Control Without A Runtime
|
|
160
|
+
|
|
161
|
+
The 3.1 async helpers cover common production workflows while keeping plain `Promise` and
|
|
162
|
+
`AbortSignal`-compatible APIs underneath. You choose the policy in the type signature instead of
|
|
163
|
+
hiding it in a helper that throws.
|
|
164
|
+
|
|
165
|
+
Retry transient work and fall back to cache only after the retry policy is exhausted:
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
const user = ResultAsync.retryOrElse(
|
|
169
|
+
(attempt, signal) => fetchUser(id, { attempt, signal }),
|
|
170
|
+
{
|
|
171
|
+
times: 2,
|
|
172
|
+
delayMs: ({ nextAttempt }) => nextAttempt * 100,
|
|
173
|
+
while: (error) => error._tag === 'RateLimitError' || error._tag === 'ServiceUnavailableError',
|
|
174
|
+
orElse: () => readCachedUser(id),
|
|
175
|
+
},
|
|
176
|
+
)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Race a primary and replica with cooperative loser abort:
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
const user = ResultAsync.race(
|
|
183
|
+
(signal) => readUserFromPrimary(id, { signal }),
|
|
184
|
+
(signal) => readUserFromReplica(id, { signal }),
|
|
185
|
+
)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Keep timeout as a typed `Err`, not a rejected promise:
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
const user = ResultAsync.timeout(
|
|
192
|
+
(signal) => readUserFromPrimary(id, { signal }),
|
|
193
|
+
{
|
|
194
|
+
timeoutMs: 1_500,
|
|
195
|
+
onTimeout: () => new FetchUserTimeoutError({ id, timeoutMs: 1_500 }),
|
|
196
|
+
},
|
|
197
|
+
)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Process batches with bounded concurrency and explicit cleanup:
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
const imported = ResultAsync.withResource({
|
|
204
|
+
acquire: (signal) => openImportSession({ signal }),
|
|
205
|
+
use: (session, signal) =>
|
|
206
|
+
ResultAsync.forEach(
|
|
207
|
+
rows,
|
|
208
|
+
(row) => importRow(session, row, { signal }),
|
|
209
|
+
{ concurrency: 8, discard: true },
|
|
210
|
+
),
|
|
211
|
+
release: (session) => session.close(),
|
|
212
|
+
})
|
|
213
|
+
```
|
|
214
|
+
|
|
136
215
|
## Quick Start
|
|
137
216
|
|
|
138
217
|
```ts
|
|
@@ -325,14 +404,31 @@ new UserNotFoundError({ id: 'usr_123' })
|
|
|
325
404
|
Use `TaggedEnum` only for lightweight tagged unions that do not need to be real errors.
|
|
326
405
|
|
|
327
406
|
```ts
|
|
407
|
+
import { taggedEnum } from 'resultar'
|
|
328
408
|
import type { TaggedEnum } from 'resultar'
|
|
329
409
|
|
|
330
410
|
type PaymentError = TaggedEnum<{
|
|
331
411
|
CardDeclined: { readonly code: string }
|
|
332
412
|
InsufficientFunds: { readonly balance: number }
|
|
333
413
|
}>
|
|
414
|
+
|
|
415
|
+
const PaymentError = taggedEnum<{
|
|
416
|
+
CardDeclined: { readonly code: string }
|
|
417
|
+
InsufficientFunds: { readonly balance: number }
|
|
418
|
+
}>()
|
|
419
|
+
|
|
420
|
+
const declined = PaymentError.CardDeclined({ code: 'card_declined' })
|
|
421
|
+
|
|
422
|
+
PaymentError.$is('CardDeclined', declined) // true
|
|
423
|
+
PaymentError.$match(declined, {
|
|
424
|
+
CardDeclined: (error) => error.code,
|
|
425
|
+
InsufficientFunds: (error) => String(error.balance),
|
|
426
|
+
})
|
|
334
427
|
```
|
|
335
428
|
|
|
429
|
+
Use `redact(value, label?)` for tagged error props that must not leak through messages or JSON.
|
|
430
|
+
Use `revealRedacted(value)` only at the exact boundary that is allowed to see the secret.
|
|
431
|
+
|
|
336
432
|
## Composing Results
|
|
337
433
|
|
|
338
434
|
Use `map` for infallible transforms:
|
|
@@ -387,6 +483,53 @@ const draftUser = createUser('bad-email').catchTag('InvalidEmailError', (error)
|
|
|
387
483
|
)
|
|
388
484
|
```
|
|
389
485
|
|
|
486
|
+
## Reasoned Tagged Errors
|
|
487
|
+
|
|
488
|
+
Use `catchReason`, `catchReasons`, and `unwrapReason` when a stable parent error has a nested tagged
|
|
489
|
+
reason. This keeps the outer error useful for logs and boundaries while allowing local recovery by
|
|
490
|
+
the precise reason.
|
|
491
|
+
|
|
492
|
+
```ts
|
|
493
|
+
import { createTaggedError, err, ok, taggedEnum } from 'resultar'
|
|
494
|
+
import type { StrictResult, TaggedEnum } from 'resultar'
|
|
495
|
+
|
|
496
|
+
const AiReason = taggedEnum<{
|
|
497
|
+
QuotaExceededError: { readonly limit: number }
|
|
498
|
+
RateLimitError: { readonly retryAfterMs: number }
|
|
499
|
+
}>()
|
|
500
|
+
|
|
501
|
+
type AiReason = TaggedEnum<{
|
|
502
|
+
QuotaExceededError: { readonly limit: number }
|
|
503
|
+
RateLimitError: { readonly retryAfterMs: number }
|
|
504
|
+
}>
|
|
505
|
+
|
|
506
|
+
class AiError extends createTaggedError({
|
|
507
|
+
name: 'AiError',
|
|
508
|
+
message: 'AI request failed',
|
|
509
|
+
}) {
|
|
510
|
+
public readonly reason: AiReason
|
|
511
|
+
|
|
512
|
+
public constructor(props: { readonly cause?: unknown; readonly reason: AiReason }) {
|
|
513
|
+
super({ cause: props.cause })
|
|
514
|
+
this.reason = props.reason
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const askAi = (): StrictResult<string, AiError> =>
|
|
519
|
+
err(new AiError({ reason: AiReason.RateLimitError({ retryAfterMs: 1_000 }) }))
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
```ts
|
|
523
|
+
const recovered = askAi().catchReason('AiError', 'RateLimitError', (reason) =>
|
|
524
|
+
ok(`retry after ${reason.retryAfterMs}ms`),
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
const unwrapped = askAi().unwrapReason('AiError')
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
`catchReason` removes only the handled reason from the remaining error type. `unwrapReason` moves the
|
|
531
|
+
reason union into the error channel when the parent tag matches.
|
|
532
|
+
|
|
390
533
|
Use `match`, `matchTags`, or `matchTagsPartial` at boundaries where a result becomes a response,
|
|
391
534
|
log entry, queue acknowledgement, CLI exit code, or UI state.
|
|
392
535
|
|
|
@@ -410,9 +553,9 @@ const response = createUser(input.email).matchTagsPartial(
|
|
|
410
553
|
|
|
411
554
|
Use fallback APIs when an expected failure has a useful recovery path.
|
|
412
555
|
|
|
413
|
-
|
|
|
556
|
+
| Fallback idea | Resultar API |
|
|
414
557
|
| --- | --- |
|
|
415
|
-
| try another
|
|
558
|
+
| try another branch | `result.orElse(() => fallbackResult)` |
|
|
416
559
|
| replace the failure | `mapErr(...)` or `orElse(() => err(newError))` |
|
|
417
560
|
| replace the failure with success | `orElse(() => ok(defaultValue))` inside a pipeline |
|
|
418
561
|
| default at the edge | `unwrapOr(defaultValue)` |
|
|
@@ -453,10 +596,147 @@ const user = ResultAsync.firstSuccessOf([
|
|
|
453
596
|
])
|
|
454
597
|
```
|
|
455
598
|
|
|
599
|
+
Use `combine` with records when independent results should keep names:
|
|
600
|
+
|
|
601
|
+
```ts
|
|
602
|
+
const context = Result.combine({
|
|
603
|
+
org: readOrg(orgId),
|
|
604
|
+
user: readUser(userId),
|
|
605
|
+
})
|
|
606
|
+
|
|
607
|
+
const validated = Result.combineWithAllErrors({
|
|
608
|
+
email: validateEmail(input.email),
|
|
609
|
+
name: validateName(input.name),
|
|
610
|
+
})
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
For lazy async traversal, `ResultAsync.forEach` and mapped `ResultAsync.validateAll` run
|
|
614
|
+
sequentially by default. Pass `{ concurrency: n }` for bounded concurrency or
|
|
615
|
+
`{ concurrency: "unbounded" }` to start every mapped task immediately. See
|
|
616
|
+
[DOCUMENTATION.md](../../DOCUMENTATION.md#async-concurrency-mapping) for async concurrency details
|
|
617
|
+
and the runtime features Resultar intentionally keeps out of core.
|
|
618
|
+
|
|
619
|
+
For concurrent async races, use lazy signal-aware tasks. `race` and `raceAll` return the first
|
|
620
|
+
success. `raceFirst` returns the first completed result, success or failure. `timeout` is a
|
|
621
|
+
`raceFirst` convenience.
|
|
622
|
+
|
|
623
|
+
```ts
|
|
624
|
+
const user = ResultAsync.timeout(
|
|
625
|
+
(signal) => fetchUser(id, { signal }),
|
|
626
|
+
{
|
|
627
|
+
timeoutMs: 2_000,
|
|
628
|
+
onTimeout: () => new FetchUserTimeoutError({ id }),
|
|
629
|
+
},
|
|
630
|
+
)
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
For transient async failures, use lazy retry tasks. The task receives a zero-based attempt number and
|
|
634
|
+
a `ResultAsyncAbortSignal`-compatible signal; `times` is the number of retries after the first
|
|
635
|
+
attempt.
|
|
636
|
+
|
|
637
|
+
```ts
|
|
638
|
+
const user = ResultAsync.retry(
|
|
639
|
+
(attempt, signal) => fetchUser(id, { attempt, signal }),
|
|
640
|
+
{
|
|
641
|
+
times: 2,
|
|
642
|
+
delayMs: ({ nextAttempt }) => nextAttempt * 100,
|
|
643
|
+
while: (error) => error._tag === 'RateLimitError',
|
|
644
|
+
},
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
const withFallback = ResultAsync.retryOrElse(
|
|
648
|
+
(attempt, signal) => fetchUser(id, { attempt, signal }),
|
|
649
|
+
{
|
|
650
|
+
times: 2,
|
|
651
|
+
orElse: () => readCachedUser(id),
|
|
652
|
+
},
|
|
653
|
+
)
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
For resourceful async work, use `ResultAsync.withResource` when acquisition and cleanup must stay
|
|
657
|
+
paired. Cleanup is best-effort and always runs after successful acquisition, including when the use
|
|
658
|
+
step returns `Err` or rejects.
|
|
659
|
+
|
|
660
|
+
```ts
|
|
661
|
+
const lines = ResultAsync.withResource({
|
|
662
|
+
acquire: (signal) => openFile(path, { signal }),
|
|
663
|
+
use: (file, signal) => readLines(file, { signal }),
|
|
664
|
+
release: (file) => file.close(),
|
|
665
|
+
})
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
For pull-based streams, keep the runtime native: expose an `AsyncIterable<Result<T, E>>` and close
|
|
669
|
+
resources in `finally`. See
|
|
670
|
+
[DOCUMENTATION.md](../../DOCUMENTATION.md#resourceful-async-iterables) for the full recipe.
|
|
671
|
+
|
|
456
672
|
Use `catchTag` / `catchTags` when recovery depends on a specific tagged error. Use
|
|
457
673
|
`matchTagsPartial` when only a boundary needs fallback mapping. Use `unwrapOr` only at final edges
|
|
458
674
|
where defaulting is intentional.
|
|
459
675
|
|
|
676
|
+
## Catching Errors
|
|
677
|
+
|
|
678
|
+
Resultar has different catch APIs for different error sources. Use the smallest API that matches
|
|
679
|
+
where the failure appears.
|
|
680
|
+
|
|
681
|
+
| Need | Use |
|
|
682
|
+
| --- | --- |
|
|
683
|
+
| Catch thrown sync code now | `tryResult(fn, toError?)` |
|
|
684
|
+
| Wrap a throwing sync function for later | `fromThrowable(fn, toError?)` |
|
|
685
|
+
| Catch an existing rejecting promise | `fromPromise(promise, toError)` |
|
|
686
|
+
| Catch a promise or async factory | `tryResultAsync(promiseOrFactory, toError?)` |
|
|
687
|
+
| Wrap an async function for later | `fromThrowableAsync(fn, toError?)` |
|
|
688
|
+
| Turn one error into another | `mapErr(fn)` |
|
|
689
|
+
| Recover with another result | `orElse(fn)` |
|
|
690
|
+
| Recover specific tagged errors | `catchTag(tag, fn)` or `catchTags(handlers)` |
|
|
691
|
+
| Recover nested tagged reasons | `catchReason(parentTag, reasonTag, fn)` or `catchReasons(parentTag, handlers)` |
|
|
692
|
+
| Convert a result at a boundary | `match`, `matchTags`, or `matchTagsPartial` |
|
|
693
|
+
| Handle an existing `Error` value | `matchError` or `matchErrorPartial` |
|
|
694
|
+
| Map thrown errors in linear code | `safeTry({ try, catch })` |
|
|
695
|
+
| Recover after retry exhaustion | `ResultAsync.retryOrElse(task, options)` |
|
|
696
|
+
|
|
697
|
+
Catch uncontrolled code at the edge:
|
|
698
|
+
|
|
699
|
+
```ts
|
|
700
|
+
const parsed: StrictResult<Config, ParseConfigError> = tryResult(
|
|
701
|
+
() => JSON.parse(input) as Config,
|
|
702
|
+
(cause) => new ParseConfigError({ cause }),
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
const response: StrictResultAsync<Response, FetchPayloadError> = tryResultAsync(
|
|
706
|
+
() => fetch(url),
|
|
707
|
+
(cause) => new FetchPayloadError({ cause, url }),
|
|
708
|
+
)
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
Thrown or rejected values are `unknown` at the boundary. The mapper decides the documented error
|
|
712
|
+
type: `ParseConfigError` for parsing and `FetchPayloadError` for fetching.
|
|
713
|
+
|
|
714
|
+
Recover inside the pipeline:
|
|
715
|
+
|
|
716
|
+
```ts
|
|
717
|
+
const user: StrictResultAsync<User, ReadUserError> = readUserFromCache(id)
|
|
718
|
+
.orElse(() => readUserFromDatabase(id))
|
|
719
|
+
.mapErr((cause) => new ReadUserError({ cause, id }))
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
The cache/database error union is normalized into one public `ReadUserError` while preserving the
|
|
723
|
+
original error as `cause`.
|
|
724
|
+
|
|
725
|
+
Handle tagged errors at the boundary:
|
|
726
|
+
|
|
727
|
+
```ts
|
|
728
|
+
const response: HttpResponse = createUser(input).matchTagsPartial(
|
|
729
|
+
(user) => ({ body: user, statusCode: 201 }),
|
|
730
|
+
{
|
|
731
|
+
InvalidEmailError: (error) => ({ body: { code: error._tag }, statusCode: 400 }),
|
|
732
|
+
},
|
|
733
|
+
(error) => ({ body: { code: error._tag }, statusCode: 500 }),
|
|
734
|
+
)
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
For the full decision map, see
|
|
738
|
+
[Catching And Recovering Errors](../../DOCUMENTATION.md#catching-and-recovering-errors).
|
|
739
|
+
|
|
460
740
|
## Wrapping Throwing Code
|
|
461
741
|
|
|
462
742
|
Use `tryResult` and `tryResultAsync` at the edge of uncontrolled code:
|
|
@@ -486,6 +766,41 @@ const user = fromPromise(loadUserFromRemote(id), (cause) => new FetchUserError({
|
|
|
486
766
|
After that edge conversion, keep your own domain functions returning `Result` values instead of
|
|
487
767
|
throwing expected failures.
|
|
488
768
|
|
|
769
|
+
## Validation Errors
|
|
770
|
+
|
|
771
|
+
Keep validation failures as normal tagged errors. Map external validators into a small issue shape
|
|
772
|
+
at the adapter boundary.
|
|
773
|
+
|
|
774
|
+
```ts
|
|
775
|
+
type ValidationIssue = {
|
|
776
|
+
readonly message: string
|
|
777
|
+
readonly path: string
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
class ValidationError extends createTaggedError({
|
|
781
|
+
name: 'ValidationError',
|
|
782
|
+
message: 'Invalid input',
|
|
783
|
+
}) {
|
|
784
|
+
public readonly issues: readonly ValidationIssue[]
|
|
785
|
+
|
|
786
|
+
public constructor(props: {
|
|
787
|
+
readonly cause?: unknown
|
|
788
|
+
readonly issues: readonly ValidationIssue[]
|
|
789
|
+
}) {
|
|
790
|
+
super({ cause: props.cause })
|
|
791
|
+
this.issues = props.issues
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const issueFromPath = (path: readonly (number | string)[], message: string): ValidationIssue => ({
|
|
796
|
+
message,
|
|
797
|
+
path: path.join('.'),
|
|
798
|
+
})
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
For Zod, Standard Schema, or any other validator, decode at the edge and map the library-specific
|
|
802
|
+
issue format into `ValidationIssue[]`. Resultar does not depend on any validator package.
|
|
803
|
+
|
|
489
804
|
## Unexpected Errors
|
|
490
805
|
|
|
491
806
|
Expected application failures belong in `Err`. Bugs, impossible states, and unrecoverable conditions
|
|
@@ -618,7 +933,7 @@ pnpm exec resultar-lint unpatch
|
|
|
618
933
|
```
|
|
619
934
|
|
|
620
935
|
For TypeScript 7 native-preview projects, `resultar-tsgo` exposes a `tsgo` wrapper that runs native
|
|
621
|
-
TypeScript and then Resultar
|
|
936
|
+
TypeScript and then Resultar lint validation.
|
|
622
937
|
|
|
623
938
|
## API Decision Guide
|
|
624
939
|
|
|
@@ -637,6 +952,8 @@ TypeScript and then Resultar no-discard validation.
|
|
|
637
952
|
| Transform error | `mapErr` |
|
|
638
953
|
| Continue with fallible work | `andThen` or `asyncAndThen` |
|
|
639
954
|
| Recover from failure | `orElse`, `catchTag`, or `catchTags` |
|
|
955
|
+
| Recover from a nested tagged reason | `catchReason` or `catchReasons` |
|
|
956
|
+
| Move a parent error reason into the error channel | `unwrapReason` |
|
|
640
957
|
| Replace failure with another failure | `mapErr` or `orElse(() => err(newError))` |
|
|
641
958
|
| Replace failure with success | `orElse(() => ok(defaultValue))` |
|
|
642
959
|
| Handle a boundary | `match`, `matchTags`, or `matchTagsPartial` |
|
|
@@ -645,11 +962,26 @@ TypeScript and then Resultar no-discard validation.
|
|
|
645
962
|
| Combine many independent results | `Result.combine` or `ResultAsync.combine` |
|
|
646
963
|
| Collect all validation failures | `validateAll` or `combineWithAllErrors` |
|
|
647
964
|
| Try fallback candidates | `firstSuccessOf` |
|
|
965
|
+
| Bound lazy async traversal | `ResultAsync.forEach` or mapped `ResultAsync.validateAll` with `{ concurrency }` |
|
|
966
|
+
| Race concurrent tasks | `ResultAsync.race`, `raceAll`, `raceFirst`, or `raceWith` |
|
|
967
|
+
| Apply a cooperative timeout | `ResultAsync.timeout` |
|
|
968
|
+
| Retry transient async work | `ResultAsync.retry` or `ResultAsync.retryOrElse` |
|
|
969
|
+
| Pair async acquisition with cleanup | `ResultAsync.withResource` |
|
|
648
970
|
| Process collections sequentially | `loop`, `iterate`, or `forEach` |
|
|
649
971
|
| Observe without changing outcome | `tap`, `tapError`, or `log` |
|
|
650
972
|
| Throw intentionally at a final edge | `unwrapOrThrow()` |
|
|
651
973
|
| Default intentionally at a final edge | `unwrapOr(defaultValue)` |
|
|
652
974
|
|
|
975
|
+
## What Stays Out Of Core
|
|
976
|
+
|
|
977
|
+
Resultar keeps the scope narrow: explicit error values, typed expected failures, and composable
|
|
978
|
+
recovery.
|
|
979
|
+
|
|
980
|
+
- No generator-based runtime DSL.
|
|
981
|
+
- No schedule engine in core.
|
|
982
|
+
- No config, cache, request resolver, resource, fiber, or service runtime.
|
|
983
|
+
- No raw `Error | T` conversion API.
|
|
984
|
+
|
|
653
985
|
## Version 3 Notes
|
|
654
986
|
|
|
655
987
|
`3.0.0` is a semver-major release:
|
|
@@ -689,11 +1021,12 @@ This repository is a pnpm workspace:
|
|
|
689
1021
|
|
|
690
1022
|
- `packages/resultar`: Resultar runtime package.
|
|
691
1023
|
- `packages/resultar-lint`: TypeScript language-service diagnostics and no-discard CLI.
|
|
692
|
-
- `packages/resultar-tsgo`: TypeScript 7 native-preview `tsgo` wrapper plus Resultar
|
|
1024
|
+
- `packages/resultar-tsgo`: TypeScript 7 native-preview `tsgo` wrapper plus Resultar lint
|
|
693
1025
|
validation.
|
|
694
1026
|
- `benchmarks`: benchmark package.
|
|
695
|
-
- `examples/resultar
|
|
696
|
-
- `examples/
|
|
1027
|
+
- `examples/resultar`: runnable core Resultar cookbook.
|
|
1028
|
+
- `examples/lint`: TypeScript 6 Resultar lint, patched `tsc`, and Vite+ / Oxlint smoke example.
|
|
1029
|
+
- `examples/tsgo-lint`: TypeScript 7 native-preview Resultar lint smoke example.
|
|
697
1030
|
|
|
698
1031
|
Common commands:
|
|
699
1032
|
|
|
@@ -705,7 +1038,7 @@ pnpm test
|
|
|
705
1038
|
pnpm run test:cov
|
|
706
1039
|
pnpm run build
|
|
707
1040
|
pnpm run check:full
|
|
708
|
-
pnpm run test:
|
|
1041
|
+
pnpm run test:examples
|
|
709
1042
|
pnpm run bench
|
|
710
1043
|
```
|
|
711
1044
|
|