Composition: pipe, composeK, safeTry
Composition utilities — barrel export.
Re-exports Kleisli composition and pipe utilities for Result pipelines.
Functions
Section titled “Functions”composeK()
Section titled “composeK()”Kleisli composition — composes N switch functions into one. Each function returns a Result, and the composed function chains them. Short-circuits on the first failure.
F# equivalent: f1 >=> f2 >=> f3
Compared to composeKAsync: this is the sync variant — each function
must return IResultOfT synchronously. For async-compatible composition
(callbacks may return Promise<IResultOfT>), use composeKAsync from
@sandlada/result/composition.
Empty input guard: calling composeK() with zero functions throws
TypeError at construction time. The async variant has the same policy.
Synchronous throw policy: if any function in the chain throws
synchronously, the catch block funnels the unknown rejection through
as unknown as IResultOfT<unknown, unknown> - the same honesty trade-off
the sync composeK and the retry module make. The composed function’s
declared error type E is therefore a structural claim only: a synchronous
throw at runtime can produce an arbitrary unknown value. If you need
precise error-type narrowing, wrap each step in tryCatch with an
errorFn that maps to your E before composing.
export function composeK<A, B, E>( f1: (a: A) => IResultOfT<B, E>,): (a: A) => IResultOfT<B, E>;export function composeK<A, B, C, E>( f1: (a: A) => IResultOfT<B, E>, f2: (b: B) => IResultOfT<C, E>,): (a: A) => IResultOfT<C, E>;export function composeK<A, B, C, D, E>( f1: (a: A) => IResultOfT<B, E>, f2: (b: B) => IResultOfT<C, E>, f3: (c: C) => IResultOfT<D, E>,): (a: A) => IResultOfT<D, E>;export function composeK<A, B, C, D, F, E>( f1: (a: A) => IResultOfT<B, E>, f2: (b: B) => IResultOfT<C, E>, f3: (c: C) => IResultOfT<D, E>, f4: (d: D) => IResultOfT<F, E>,): (a: A) => IResultOfT<F, E>;export function composeK<A, B, C, D, F, G, E>( f1: (a: A) => IResultOfT<B, E>, f2: (b: B) => IResultOfT<C, E>, f3: (c: C) => IResultOfT<D, E>, f4: (d: D) => IResultOfT<F, E>, f5: (f: F) => IResultOfT<G, E>,): (a: A) => IResultOfT<G, E>;export function composeK<A, B, C, D, F, G, H, E>( f1: (a: A) => IResultOfT<B, E>, f2: (b: B) => IResultOfT<C, E>, f3: (c: C) => IResultOfT<D, E>, f4: (d: D) => IResultOfT<F, E>, f5: (f: F) => IResultOfT<G, E>, f6: (g: G) => IResultOfT<H, E>,): (a: A) => IResultOfT<H, E>;Defined in: composition/composeK.ts
Example
Section titled “Example”import { composeK } from '@sandlada/result/composition';import { ok, err } from '@sandlada/result/factories';import { match } from '@sandlada/result/operators';
const run = composeK( (x: number) => ok(x * 2), (x: number) => (x > 50 ? ok(x) : err('too small')),);const input = 30;const result = run(input);const label = match((n) => `ok:${n}`, (e) => `err:${String(e)}`, result);// label === 'ok:60'
const short = run(10);const blocked = match((n) => `ok:${n}`, (e) => `err:${String(e)}`, short);// blocked === 'err:too small'; composeK stops at the first ErrThrows
Section titled “Throws”If called with zero functions.
Throws
Section titled “Throws”If any composed step throws synchronously, the thrown value
is captured into the returned failure’s error field — but
its static type widens to unknown, not the declared E.
Use tryCatch(..., errorFn) per step if you need typed errors.
composeKAsync()
Section titled “composeKAsync()”Kleisli composition for async switch functions. Each function can return IResultOfT or Promise<IResultOfT>.
F# equivalent: f1 >=> f2 >=> f3 (async)
Synchronous throw policy: if any function in the chain throws
synchronously, the catch block funnels the unknown rejection through
as unknown as IResultOfT<unknown, unknown> - the same honesty trade-off
the sync composeK and the retry module make. The composed function’s
declared error type E is therefore a structural claim only:
a synchronous throw at runtime can produce an arbitrary unknown value.
export function composeKAsync<A, B, E>( f1: (a: A) => IResultOfT<B, E> | Promise<IResultOfT<B, E>>,): (a: A) => Promise<IResultOfT<B, E>>;export function composeKAsync<A, B, C, E>( f1: (a: A) => IResultOfT<B, E> | Promise<IResultOfT<B, E>>, f2: (b: B) => IResultOfT<C, E> | Promise<IResultOfT<C, E>>,): (a: A) => Promise<IResultOfT<C, E>>;export function composeKAsync<A, B, C, D, E>( f1: (a: A) => IResultOfT<B, E> | Promise<IResultOfT<B, E>>, f2: (b: B) => IResultOfT<C, E> | Promise<IResultOfT<C, E>>, f3: (c: C) => IResultOfT<D, E> | Promise<IResultOfT<D, E>>,): (a: A) => Promise<IResultOfT<D, E>>;export function composeKAsync<A, B, C, D, F, E>( f1: (a: A) => IResultOfT<B, E> | Promise<IResultOfT<B, E>>, f2: (b: B) => IResultOfT<C, E> | Promise<IResultOfT<C, E>>, f3: (c: C) => IResultOfT<D, E> | Promise<IResultOfT<D, E>>, f4: (d: D) => IResultOfT<F, E> | Promise<IResultOfT<F, E>>,): (a: A) => Promise<IResultOfT<F, E>>;export function composeKAsync<A, B, C, D, F, G, E>( f1: (a: A) => IResultOfT<B, E> | Promise<IResultOfT<B, E>>, f2: (b: B) => IResultOfT<C, E> | Promise<IResultOfT<C, E>>, f3: (c: C) => IResultOfT<D, E> | Promise<IResultOfT<D, E>>, f4: (d: D) => IResultOfT<F, E> | Promise<IResultOfT<F, E>>, f5: (f: F) => IResultOfT<G, E> | Promise<IResultOfT<G, E>>,): (a: A) => Promise<IResultOfT<G, E>>;export function composeKAsync<A, B, C, D, F, G, H, E>( f1: (a: A) => IResultOfT<B, E> | Promise<IResultOfT<B, E>>, f2: (b: B) => IResultOfT<C, E> | Promise<IResultOfT<C, E>>, f3: (c: C) => IResultOfT<D, E> | Promise<IResultOfT<D, E>>, f4: (d: D) => IResultOfT<F, E> | Promise<IResultOfT<F, E>>, f5: (f: F) => IResultOfT<G, E> | Promise<IResultOfT<G, E>>, f6: (g: G) => IResultOfT<H, E> | Promise<IResultOfT<H, E>>,): (a: A) => Promise<IResultOfT<H, E>>;Defined in: composition/composeKAsync.ts
Example
Section titled “Example”import { composeKAsync } from '@sandlada/result/composition';import { asyncOk, asyncErr } from '@sandlada/result/factories';import { matchAsync } from '@sandlada/result/promise-result';
const run = composeKAsync( (x: number) => asyncOk(x * 2), (x: number) => (x > 50 ? asyncOk(x) : asyncErr('too small')),);const input = 30;const label = await matchAsync( (n) => `ok:${n}`, (e) => `err:${String(e)}`, run(input),);// label === 'ok:60'
const blocked = await matchAsync( (n) => `ok:${n}`, (e) => `err:${String(e)}`, run(10),);// blocked === 'err:too small'; composeKAsync stops at the first ErrThrows
Section titled “Throws”If called with zero functions.
Throws
Section titled “Throws”If any composed step throws synchronously or rejects
asynchronously, the thrown/rejected value is captured
into the returned failure’s error field — but its
static type widens to unknown, not the declared E.
Use tryCatchAsync(..., errorFn) per step if you need
typed errors.
fromSafeTry()
Section titled “fromSafeTry()”Evaluates a generator function that uses yield* safeTry(...) and
collects the final IResultOfT.
- If the generator returns a value: the value is wrapped in
ok(). - If the generator returns a Result: that Result is used as-is, so the
documented
return ok(undefined)workaround yieldsOk(undefined)instead of a doubly wrapped Result. - If the generator yields a value: that yield is treated as a propagated
failure and returned as-is. The yielded value must be a Result - a yield
that is not one throws a
TypeError, because the runner would otherwise hand back a value that is not a Result at all.
A generator that yields twice throws - the runner probes a raw yield for a second one before reporting the failure.
export function fromSafeTry<T, E>( gen: () => Generator<IResultOfT<never, E>, IResultOfT<T, E>, unknown>,): IResultOfT<T, E>;export function fromSafeTry<T, E>( gen: () => Generator<IResultOfT<never, E>, T | undefined, unknown>,): IResultOfT<T, E>;Defined in: composition/safeTry.ts
Example
Section titled “Example”import { fromSafeTry, safeTry } from '@sandlada/result/composition';import { ok, err } from '@sandlada/result/factories';import { match } from '@sandlada/result/operators';
const step = ok(42);const result = fromSafeTry(function* () { const value = yield* safeTry(step); return value;});const label = match((n) => `ok:${n}`, (e) => `err:${String(e)}`, result);// label === 'ok:42'
const aborted = fromSafeTry(function* () { yield* safeTry(err('boom')); return 0;});const blocked = match((n) => `ok:${n}`, (e) => `err:${String(e)}`, aborted);// blocked === 'err:boom'; fromSafeTry returns the propagated failurefromSafeTryAsync()
Section titled “fromSafeTryAsync()”Runs an async generator that uses yield* safeTryAsync(...) and returns a
lazy AsyncResult collecting the final IResultOfT.
- If the generator returns a value: the value is wrapped in
Okwhen.run()is called. - If the generator returns a Result: that Result is used as-is, so the
documented
return asyncOk(undefined)workaround yieldsOk(undefined)instead of a doubly wrapped Result. - If the generator yields a value: that yield is treated as a propagated
failure and returned as-is. The yielded value must be a Result - a yield
that is not one throws a
TypeError, because the runner would otherwise hand back a value that is not a Result at all.
Throws when the generator returns undefined without yielding, or yields
more than once.
export function fromSafeTryAsync<T, E>( gen: () => AsyncGenerator<IResultOfT<never, E>, IResultOfT<T, E>, unknown>,): AsyncResult<T, E>;export function fromSafeTryAsync<T, E>( gen: () => AsyncGenerator<IResultOfT<never, E>, T | undefined, unknown>,): AsyncResult<T, E>;Defined in: composition/safeTryAsync.ts
pipe()
Section titled “pipe()”Pipes a value through a sequence of functions (left-to-right composition). Each function receives the output of the previous one.
F# equivalent: value |> fn1 |> fn2 |> fn3
export function pipe<A>(value: A): A;export function pipe<A, B>(value: A, fn1: (a: A) => B): B;export function pipe<A, B, C>(value: A, fn1: (a: A) => B, fn2: (b: B) => C): C;export function pipe<A, B, C, D>(value: A, fn1: (a: A) => B, fn2: (b: B) => C, fn3: (c: C) => D): D;export function pipe<A, B, C, D, E>( value: A, fn1: (a: A) => B, fn2: (b: B) => C, fn3: (c: C) => D, fn4: (d: D) => E,): E;export function pipe<A, B, C, D, E, F>( value: A, fn1: (a: A) => B, fn2: (b: B) => C, fn3: (c: C) => D, fn4: (d: D) => E, fn5: (e: E) => F,): F;export function pipe<A, B, C, D, E, F, G>( value: A, fn1: (a: A) => B, fn2: (b: B) => C, fn3: (c: C) => D, fn4: (d: D) => E, fn5: (e: E) => F, fn6: (f: F) => G,): G;export function pipe<A, B, C, D, E, F, G, H>( value: A, fn1: (a: A) => B, fn2: (b: B) => C, fn3: (c: C) => D, fn4: (d: D) => E, fn5: (e: E) => F, fn6: (f: F) => G, fn7: (g: G) => H,): H;export function pipe<A, B, C, D, E, F, G, H, I>( value: A, fn1: (a: A) => B, fn2: (b: B) => C, fn3: (c: C) => D, fn4: (d: D) => E, fn5: (e: E) => F, fn6: (f: F) => G, fn7: (g: G) => H, fn8: (h: H) => I,): I;export function pipe<A, B, C, D, E, F, G, H, I, J>( value: A, fn1: (a: A) => B, fn2: (b: B) => C, fn3: (c: C) => D, fn4: (d: D) => E, fn5: (e: E) => F, fn6: (f: F) => G, fn7: (g: G) => H, fn8: (h: H) => I, fn9: (i: I) => J,): J;export function pipe<A, B, C, D, E, F, G, H, I, J, K>( value: A, fn1: (a: A) => B, fn2: (b: B) => C, fn3: (c: C) => D, fn4: (d: D) => E, fn5: (e: E) => F, fn6: (f: F) => G, fn7: (g: G) => H, fn8: (h: H) => I, fn9: (i: I) => J, fn10: (j: J) => K,): K;Defined in: composition/pipe.ts
Example
Section titled “Example”import { pipe } from '@sandlada/result/composition';import { map, bind, match } from '@sandlada/result/operators';import { ok, err } from '@sandlada/result/factories';
const input = ok(42);const outcome = pipe( input, map((x: number) => x * 2), bind((x: number) => (x > 50 ? ok(x) : err('too small'))), match((v) => `OK: ${v}`, (e) => `Error: ${e}`),);// outcome === 'OK: 84'
const blocked = pipe( ok(10), map((x: number) => x * 2), bind((x: number) => (x > 50 ? ok(x) : err('too small'))), match((v) => `OK: ${v}`, (e) => `Error: ${e}`),);// blocked === 'Error: too small'; bind short-circuits before match sees a successpipeAsync()
Section titled “pipeAsync()”Async version of pipe. Pipes a value through a sequence of async functions. Each function receives the output of the previous one.
export function pipeAsync<A>(value: A): Promise<A>;export function pipeAsync<A, B>(value: A, fn1: (a: A) => B): Promise<B>;export function pipeAsync<A, B, C>(value: A, fn1: (a: A) => B, fn2: (b: B) => C): Promise<C>;export function pipeAsync<A, B, C, D>(value: A, fn1: (a: A) => B, fn2: (b: B) => C, fn3: (c: C) => D): Promise<D>;export function pipeAsync<A, B, C, D, E>( value: A, fn1: (a: A) => B, fn2: (b: B) => C, fn3: (c: C) => D, fn4: (d: D) => E,): Promise<E>;export function pipeAsync<A, B, C, D, E, F>( value: A, fn1: (a: A) => B, fn2: (b: B) => C, fn3: (c: C) => D, fn4: (d: D) => E, fn5: (e: E) => F,): Promise<F>;export function pipeAsync<A, B, C, D, E, F, G>( value: A, fn1: (a: A) => B, fn2: (b: B) => C, fn3: (c: C) => D, fn4: (d: D) => E, fn5: (e: E) => F, fn6: (f: F) => G,): Promise<G>;export function pipeAsync<A, B, C, D, E, F, G, H>( value: A, fn1: (a: A) => B, fn2: (b: B) => C, fn3: (c: C) => D, fn4: (d: D) => E, fn5: (e: E) => F, fn6: (f: F) => G, fn7: (g: G) => H,): Promise<H>;export function pipeAsync<A, B, C, D, E, F, G, H, I>( value: A, fn1: (a: A) => B, fn2: (b: B) => C, fn3: (c: C) => D, fn4: (d: D) => E, fn5: (e: E) => F, fn6: (f: F) => G, fn7: (g: G) => H, fn8: (h: H) => I,): Promise<I>;export function pipeAsync<A, B, C, D, E, F, G, H, I, J>( value: A, fn1: (a: A) => B, fn2: (b: B) => C, fn3: (c: C) => D, fn4: (d: D) => E, fn5: (e: E) => F, fn6: (f: F) => G, fn7: (g: G) => H, fn8: (h: H) => I, fn9: (i: I) => J,): Promise<J>;export function pipeAsync<A, B, C, D, E, F, G, H, I, J, K>( value: A, fn1: (a: A) => B, fn2: (b: B) => C, fn3: (c: C) => D, fn4: (d: D) => E, fn5: (e: E) => F, fn6: (f: F) => G, fn7: (g: G) => H, fn8: (h: H) => I, fn9: (i: I) => J, fn10: (j: J) => K,): Promise<K>;Defined in: composition/pipeAsync.ts
Example
Section titled “Example”import { pipeAsync } from '@sandlada/result/composition';import { asyncOk, asyncErr } from '@sandlada/result/factories';import { mapAsync, bindAsync, matchAsync } from '@sandlada/result/promise-result';
const input = asyncOk(42);const outcome = await pipeAsync( input, mapAsync((x: number) => x * 2), bindAsync((x: number) => (x > 50 ? asyncOk(x) : asyncErr('too small'))), matchAsync((v) => `OK: ${v}`, (e) => `Error: ${e}`),);// outcome === 'OK: 84'
const blocked = await pipeAsync( asyncOk(10), mapAsync((x: number) => x * 2), bindAsync((x: number) => (x > 50 ? asyncOk(x) : asyncErr('too small'))), matchAsync((v) => `OK: ${v}`, (e) => `Error: ${e}`),);// blocked === 'Error: too small'; bindAsync short-circuits before matchAsync runs on successsafeTry()
Section titled “safeTry()”Generator-based yield* error propagation for Result pipelines.
safeTry wraps a Result into a Generator. On success it returns the value;
on failure it yields the error, propagating it up to a fromSafeTry runner.
This enables flat, non-nested error handling in complex sequential logic.
Wraps a IResultOfT<T, E> into a generator for use with yield*:
- On success: the generator returns the value —
yield*evaluates to it. - On failure: the generator yields the error result —
fromSafeTrycatches it.
Returns T when the inner result is Ok, otherwise yields the failure to be
collected by fromSafeTry. The Generator’s return type is T | undefined:
the success path returns T, and the failure path’s unreachable tail
explicitly returns undefined (matches JS semantics when a generator
exhausts after a yield without a top-level return).
The failure is yielded, and the generator throws when it is resumed
past that yield. A runner that resumes a delegation therefore ends the
generator instead of running the body after the failed yield* - the
short-circuit contract holds without any bookkeeping that would have to be
attributed to one specific run.
export function* safeTry<T, E>( result: IResultOfT<T, E>,): Generator<IResultOfT<never, E>, T | undefined, unknown>;Defined in: composition/safeTry.ts
Example
Section titled “Example”import { safeTry, fromSafeTry } from '@sandlada/result/composition';import { ok, err } from '@sandlada/result/factories';import { match, map } from '@sandlada/result/operators';
const step = ok(42);const success = fromSafeTry(function* () { const value = yield* safeTry(step); return value;});const doubled = map((n: number) => n * 2, success);const label = match((n) => `ok:${n}`, (e) => `err:${String(e)}`, doubled);// label === 'ok:84'
const failure = fromSafeTry(function* () { yield* safeTry(err('skip')); return 0;});const blocked = match((n) => `ok:${n}`, (e) => `err:${String(e)}`, failure);// blocked === 'err:skip'; safeTry yields Err so code after yield* never runssafeTryAsync()
Section titled “safeTryAsync()”Async Generator-based yield* error propagation for AsyncResult pipelines.
safeTryAsync wraps an AsyncResult (or a Promise<IResultOfT>) into an
AsyncGenerator. On success it returns the value; on failure it yields the
error so that fromSafeTryAsync can short-circuit the pipeline and return
the error result.
Returns T when the inner result is Ok, otherwise yields the failure to be
collected by fromSafeTryAsync. The AsyncGenerator’s return type is
T | undefined: the success path returns T, and the failure path’s
unreachable tail returns undefined (matches JS semantics when a generator
exhausts after a yield without a top-level return).
The failure is yielded, and the generator throws when it is resumed
past that yield. A runner that resumes a delegation therefore ends the
generator instead of running the body after the failed yield* - the
short-circuit contract holds without any bookkeeping that would have to be
attributed to one specific run, which matters when several runs are in
flight at the same time.
export async function* safeTryAsync<T, E>( result: AsyncResult<T, E> | Promise<IResultOfT<T, E>>,): AsyncGenerator<IResultOfT<never, E>, T | undefined, unknown>;Defined in: composition/safeTryAsync.ts
Example
Section titled “Example”import { safeTryAsync, fromSafeTryAsync } from '@sandlada/result/composition';import { asyncOk, asyncErr } from '@sandlada/result/factories';import { matchAsync } from '@sandlada/result/promise-result';
const first = asyncOk(40);const second = asyncOk(2);const pipeline = fromSafeTryAsync(async function* () { const a = yield* safeTryAsync(first); const b = yield* safeTryAsync(second); return Number(a) + Number(b);});const label = await matchAsync( (n) => `ok:${n}`, (e) => `err:${String(e)}`, pipeline.run(),);// label === 'ok:42'
const failure = fromSafeTryAsync(async function* () { yield* safeTryAsync(asyncErr('boom')); return 0;});const blocked = await matchAsync( (n) => `ok:${n}`, (e) => `err:${String(e)}`, failure.run(),);// blocked === 'err:boom'; safeTryAsync short-circuits before return 0 runs