Reliability: retry, timeout, race
Reliability — barrel export.
Re-exports all retry/timeout/concurrency helpers for production-grade ROP pipelines.
Interfaces
Section titled “Interfaces”AbortedError
Section titled “AbortedError”Defined in: reliability/retry.ts:78
Default shape of the error produced when the retry loop never invokes fn
(pre-aborted signal, or a non-finite / negative times).
Properties
Section titled “Properties”| Property | Modifier | Type |
|---|---|---|
kind |
readonly |
"Aborted" |
reason |
readonly |
unknown |
times |
readonly |
number |
EmptyInputsError
Section titled “EmptyInputsError”Defined in: reliability/race.ts:9
Default shape of the error produced by race when the input array is empty.
Library consumers can extend, narrow, or replace it via the onEmpty hook.
Properties
Section titled “Properties”| Property | Modifier | Type |
|---|---|---|
kind |
readonly |
"EmptyInputs" |
RetryOptions
Section titled “RetryOptions”Defined in: reliability/retry.ts:15
Options for retry and retryLazy.
Each field has a sensible default; only set the knobs you actually need.
Type Parameters
Section titled “Type Parameters”| Type Parameter | Default type | Description |
|---|---|---|
E |
unknown |
— the error type your fn returns in its Err. |
TE |
ThrownError |
— the error produced when fn (or one of these hooks) throws. Defaults to ThrownError; override with onThrow. |
AE |
AbortedError |
— the error produced when the loop never runs fn at all. Defaults to AbortedError; override with onAborted. |
Properties
Section titled “Properties”| Property | Modifier | Type | Description |
|---|---|---|---|
delayMs? |
readonly |
number | ((attempt, error) => number) |
Delay between attempts in milliseconds. Either a fixed number or a function of (zero-based attempt index, last error). Default 0 (no delay). Negative values are clamped to 0. |
onAborted? |
readonly |
(reason, times) => AE |
Optional factory invoked when the retry loop exits without ever calling fn (pre-aborted signal or non-finite / negative times). The returned value becomes the error of the resolved Err. Without it the library falls back to AbortedError. |
onRetry? |
readonly |
(error, attempt) => void |
Optional hook invoked after shouldRetry approves a retry and before the backoff delay begins. Useful for logging or metrics (“will retry in Nms”); the value it returns is ignored. |
onThrow? |
readonly |
(thrown) => TE |
Optional factory that converts a value thrown by fn (or by one of the hooks above) into your own error type, collapsing `E |
shouldRetry? |
readonly |
(error, attempt) => boolean |
Predicate that decides whether to retry after a given failure. Return false to stop retrying immediately and return the last result. Default: always retry. Receives `E |
signal? |
readonly |
AbortSignal |
Abort signal. If signal.aborted becomes true during the delay window, the loop exits and the last result is returned (the supplied function is never re-invoked past that point). |
times? |
readonly |
number |
Maximum retry attempts (excluding the first attempt). Default 3. Fractional values are floored — times: 2.7 performs 3 attempts total. |
ThrownError
Section titled “ThrownError”Defined in: reliability/retry.ts:69
Default shape of the error produced when fn throws instead of returning an
Err. The original thrown value is preserved verbatim in thrown, so the
Error instance, its stack and its cause all survive.
Properties
Section titled “Properties”| Property | Modifier | Type |
|---|---|---|
kind |
readonly |
"Thrown" |
thrown |
readonly |
unknown |
TimeoutError
Section titled “TimeoutError”Defined in: reliability/timeout.ts:8
Default shape of the error produced by timeout when no factory is given.
Library consumers can extend, narrow, or replace it via the onTimeout hook.
Properties
Section titled “Properties”| Property | Modifier | Type |
|---|---|---|
kind |
readonly |
"Timeout" |
ms |
readonly |
number |
Type Aliases
Section titled “Type Aliases”Settled
Section titled “Settled”Settled<
T,E> = {error?:never;ok:true;value:T; } | {error:E;kind?:"Err";ok:false;value?:never; } | {error:unknown;kind:"Rejected";ok:false;value?:never; }
Defined in: reliability/allSettled.ts:20
Discriminated outcome of a single thunk in an allSettled batch.
Three variants — discriminated by ok and, for failures, by the kind
tag:
{ ok: true, value: T }— the thunk resolved withOk.{ ok: false, error: E }— the thunk resolved withErr.{ ok: false, kind: 'Rejected', error: unknown }— the inner.run()rejected the Promise (an upstream contract violation thatallSettleddefends against). The rejection value is preserved verbatim asunknownso consumers can narrow onkindbefore readingerror. This avoids the type lie where rejection values (which can bestring,undefined, or anything else) were cast into the user’sEunion.
Type Parameters
Section titled “Type Parameters”| Type Parameter |
|---|
T |
E |
Functions
Section titled “Functions”allSettled()
Section titled “allSettled()”allSettled — never short-circuits. Collects every thunk’s outcome into a
discriminated array that mirrors the input order. Always returns Ok with the
collected list, so observability and batch coordination layers can log
everything without losing partial successes.
Run every thunk; the result is always Ok([...settled, ...in input order]).
Unhandled rejections are captured as { ok: false, error: rejection } rather than
propagated.
export function allSettled<T, E>( results: readonly AsyncResult<T, E>[],): AsyncResult<Settled<T, E>[], never>;Defined in: reliability/allSettled.ts
Example
Section titled “Example”import { allSettled } from '@sandlada/result/reliability';import { fromResult } from '@sandlada/result/async-result';import { ok, err } from '@sandlada/result/factories';
const ar = allSettled([fromResult(ok(1)), fromResult(err('a')), fromResult(ok(2))]);const r = await ar.run();// Ok([// { ok: true, value: 1 },// { ok: false, error: 'a' },// { ok: true, value: 2 },// ])Promise.any-style combination: succeeds with all collected successes (in completion order), or fails with every collected error (in completion order).
- If at least one thunk resolves with
Ok, the final result isOk([...all-successes]). - If every thunk resolves with
Err, the final result isErr([...all-errors]).
any is lazy and does not short-circuit — all thunks are guaranteed to run.
Order: successes and errors are populated in the order their underlying
runs settle (microtask scheduling), not input order. Use allSettled if input
order matters.
export function any<T, E>( results: readonly AsyncResult<T, E>[],): AsyncResult<T[], AnyError<E>[]>;Defined in: reliability/any.ts
Example
Section titled “Example”import { any } from '@sandlada/result/reliability';import { fromResult } from '@sandlada/result/async-result';import { ok, err } from '@sandlada/result/factories';
const ar = any([fromResult(ok(1)), fromResult(err('a')), fromResult(ok(2))]);const r = await ar.run(); // Ok([1, 2]) — partial success collected.race()
Section titled “race()”First-success-wins combination of AsyncResults. Returns the first thunk to
resolve with Ok; if every thunk resolves with Err, returns the first error
(in input order). Lazy — none of the inputs run until .run() is called.
Empty input: if results is empty, there is no input value or input error to
propagate, so race produces an error of its own. Rather than fabricating a value
of the caller’s E (which would be a type lie), the error type is widened to
E | EE, where EE defaults to EmptyInputsError. Supply onEmpty to
substitute a domain-specific error instead.
That widening is charged only where it can actually happen: passing a literal
non-empty array proves the empty branch unreachable, so the caller keeps a clean
AsyncResult<T, E>. Only a dynamically-sized array pays the | EE.
Selection policy — total and deterministic, independent of arrival order except where a race inherently depends on it:
- The first run to resolve
Okwins, regardless of input order. - If no run succeeds, a genuine
Erralways outranks a Promise rejection: a rejection breaks theAsyncResultcontract (which requires resolving toOk/Err) and therefore signals an upstream bug, not a domain outcome. - Among
Errs, the lowest input index wins, even if it settles last. - If every run rejected, the earliest-arriving rejection wins.
- For “first to settle, whatever it is” semantics, use a separate primitive.
Type lie: when every run rejected, the resulting Err.error is the
unknown rejection reason (cast to E in the implementation). The
static type still says E | EE. Consumers that need to distinguish an
upstream rejection from a domain Err should narrow at runtime
(e.g. (r.error as { message?: unknown }).message !== undefined or
instanceof Error). The cast is intentional: a true unknown here
would erase all type information from the success path’s E.
export function race<T, E>( results: readonly [AsyncResult<T, E>, ...AsyncResult<T, E>[]],): AsyncResult<T, E>;export function race<T, E, EE = EmptyInputsError>( results: readonly AsyncResult<T, E>[], onEmpty?: () => EE,): AsyncResult<T, E | EE>;Defined in: reliability/race.ts
Examples
Section titled “Examples”import { race } from '@sandlada/result/reliability';import { fromResult } from '@sandlada/result/async-result';import { ok, err } from '@sandlada/result/factories';
const ar = race([fromResult(ok(1)), fromResult(err('a'))]);const r = await ar.run(); // Ok(1) — first success wins.Discriminating the empty-input case
import { race } from '@sandlada/result/reliability';import { fromResult } from '@sandlada/result/async-result';import { ok } from '@sandlada/result/factories';
const single = await race([fromResult(ok(1))]).run(); // Ok(1)
// …or map the empty-input case onto your own error union:const custom = await race<number, Error, { readonly tag: 'NoCandidates' }>( [], () => ({ tag: 'NoCandidates' }),).run();retry()
Section titled “retry()”Retries a fallible function with configurable attempts, backoff, and predicate gating.
retry is eager: it returns Promise<IResultOfT<T, E>> and runs the supplied
function up to times + 1 times. Use shouldRetry to filter transient errors
(timeouts, network blips) and signal to abort the retry loop.
Synchronous throws AND promise rejections from fn are converted to Err,
as are throws escaping shouldRetry, onRetry, delayMs, onThrow and
onAborted. The returned promise therefore never rejects — matching the
AsyncResult contract used elsewhere in the library.
The retry loop respects AbortSignal between attempts only; it cannot
interrupt an in-flight invocation.
Error identity: a value thrown by fn is preserved verbatim inside a
ThrownError ({ kind: 'Thrown', thrown }), so the original Error
instance, its stack and its cause all survive. Pass onThrow to map the
throw onto your own error type instead.
Aborted / no-attempt cases: when the loop never invokes fn (a
pre-aborted signal or a non-finite / negative times), the resolved Err
carries an AbortedError ({ kind: 'Aborted', reason, times }).
Pass onAborted to substitute your own shape.
Error channels — the resolved error is E | TE | AE, where each arm is
separately discriminable and separately collapsible:
E— anErryourfnreturned.TE— something threw. Defaults to ThrownError, which keeps the thrown value verbatim; passonThrowto fold it intoE.AE— the loop never ranfnat all. Defaults to AbortedError; passonAbortedto fold it intoE.
If a caller-supplied onThrow / onAborted factory itself throws, the
library falls back to the corresponding default sentinel rather than
rejecting — a broken factory must not take the whole contract down.
export async function retry<T, E, TE = ThrownError, AE = AbortedError>( fn: () => IResultOfT<T, E> | Promise<IResultOfT<T, E>>, options: RetryOptions<E, TE, AE> = {},): Promise<IResultOfT<T, E | TE | AE>>;Defined in: reliability/retry.ts
Examples
Section titled “Examples”import { retry } from '@sandlada/result/reliability';import { ok, err } from '@sandlada/result/factories';
const r = await retry( () => Promise.resolve(ok(42)), { times: 3, delayMs: n => 50 * (n + 1) }, // linear backoff);// Ok(42); on failure r.error: E | ThrownError | AbortedError — each narrowable.Collapsing every channel onto one domain error
import { retry } from '@sandlada/result/reliability';import { ok } from '@sandlada/result/factories';
type MyError = | { readonly kind: 'Unexpected'; readonly thrown: unknown } | { readonly kind: 'Cancelled'; readonly reason: unknown };
const r = await retry<number, MyError, MyError, MyError>( () => Promise.resolve(ok(42)), { onThrow: (thrown) => ({ kind: 'Unexpected', thrown }), onAborted: (reason) => ({ kind: 'Cancelled', reason }), },);// r.error: MyErrorretryLazy()
Section titled “retryLazy()”Lazy counterpart to retry — wraps an AsyncResult and defers
execution until the returned thunk is run(). Use when an existing AsyncResult
pipeline should retry transparently without changing upstream code.
Error channels mirror the eager retry: E | TE | AE, where TE
covers throws and AE covers the never-ran case. Supply onThrow /
onAborted to collapse them onto your own error type.
Error identity: like retry, a thrown value is preserved verbatim
inside a ThrownError ({ kind: 'Thrown', thrown }) — the original Error
instance, its stack and its cause all survive. Pass onThrow to map the
throw onto your own error type.
Attempt numbering: the attempt parameter passed to shouldRetry and
onRetry is zero-based (0 = first retry attempt after the initial call).
The total number of invocations is options.times + 1 (initial + retries).
export function retryLazy<T, E, TE = ThrownError, AE = AbortedError>( ar: AsyncResult<T, E>, options: RetryOptions<E, TE, AE> = {},): AsyncResult<T, E | TE | AE>;Defined in: reliability/retryLazy.ts
Example
Section titled “Example”import { retryLazy } from '@sandlada/result/reliability';import { fromPromise } from '@sandlada/result/async-result';import { ok } from '@sandlada/result/factories';
const pipeline = retryLazy(fromPromise(() => Promise.resolve(ok(42))), { times: 3 });const r = await pipeline.run();timeout()
Section titled “timeout()”Races an AsyncResult against a timeout. If the inner result does not settle
before ms milliseconds have elapsed, the returned AsyncResult resolves to an
Err produced by the optional onTimeout factory. The default factory yields
{ kind: 'Timeout', ms }.
timeout is lazy — it never triggers ar.run() until .run() is invoked.
Cancellation caveat: JavaScript Promises cannot be forcibly cancelled.
When the timer fires first, timeout returns Err(onTimeout(ms)) but the
inner ar.run() continues executing in the background — its eventual
settlement is discarded. For long-running or resource-heavy inner work
(network I/O, file I/O, large computations), this is a resource leak. If
cooperative cancellation is required, wrap the inner work in a cancellable
primitive (e.g. one that listens to an AbortSignal) before passing it to
timeout.
export function timeout<T, E, TOE = TimeoutError>( ms: number, ar: AsyncResult<T, E>, onTimeout: (ms: number) => TOE = defaultOnTimeout as (ms: number) => TOE,): AsyncResult<T, E | TOE>;Defined in: reliability/timeout.ts
Example
Section titled “Example”import { timeout } from '@sandlada/result/reliability';import { fromPromise } from '@sandlada/result/async-result';
const safe = timeout(2000, fromPromise(() => fetch('/slow').then(r => r.json())));const r = await safe.run();// r is Ok(...) if fetch completed within 2000ms, else Err({ kind: 'Timeout', ms: 2000 }).timeoutEager()
Section titled “timeoutEager()”Eager counterpart to timeout. Wraps a () => Promise<IResultOfT>
to short-circuit slow runs into Err(onTimeout(ms)). Use this instead of
timeout when the upstream API returns a Promise<IResultOfT> rather than a
lazy AsyncResult thunk.
Accepts a () => Promise<IResultOfT<T, E>> and races it against the configured
timeout window. Reuses the same default TimeoutError shape as timeout.
Sync-throw safety: a synchronous throw from fn is converted to a
rejecting Promise via Promise.resolve().then(fn), which timeout’s
rejection handler then converts to Err(thrown) — preserving the
AsyncResult no-rejection contract.
export function timeoutEager<T, E, TOE = TimeoutError>( ms: number, fn: () => Promise<IResultOfT<T, E>>, onTimeout?: (ms: number) => TOE,): Promise<IResultOfT<T, E | TOE>>;Defined in: reliability/timeoutEager.ts
Example
Section titled “Example”import { timeoutEager } from '@sandlada/result/reliability';import { fromPromise } from '@sandlada/result/factories';
const r = await timeoutEager(2000, () => fromPromise(fetch('/slow')));