Skip to content

Composition: pipe, composeK, safeTry

Composition utilities — barrel export.

Re-exports Kleisli composition and pipe utilities for Result pipelines.

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 (G1): 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 documented in retry.ts:toErrFailure (Batch 5). 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, 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

import { composeK } from '@sandlada/result/composition';
import { ok, err } from '@sandlada/result/factories';
const p = composeK(
(x: number) => ok(x * 2),
(x: number) => x > 50 ? ok(x) : err('too small'),
);
p(30); // Ok(60)

If called with zero functions.

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.


Kleisli composition for async switch functions. Each function can return IResultOfT or Promise<IResultOfT>.

F# equivalent: f1 >=> f2 >=> f3 (async)

Synchronous throw policy (G1): 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 as the sync composeK and retry.ts:toErrFailure (Batch 5). 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

import { composeKAsync } from '@sandlada/result/composition';
import { asyncOk, asyncErr } from '@sandlada/result/promise-result';
const p = composeKAsync(
(x: number) => asyncOk(x * 2),
(x: number) => x > 50 ? asyncOk(x) : asyncErr('too small'),
);
await p(30); // Ok(60)

If called with zero functions.

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.


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 yields a value: that yield is treated as a propagated failure and returned as-is.
export function fromSafeTry<T, E>(
gen: () => Generator<IResultOfT<never, E>, T | undefined, unknown>,
): IResultOfT<T, E>;

Defined in: composition/safeTry.ts

import { fromSafeTry, safeTry } from '@sandlada/result/composition';
import { ok } from '@sandlada/result/factories';
const result = fromSafeTry(function* () {
const value = yield* safeTry(ok(42));
return value;
});
// Ok(42)

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 Ok when .run() is called.
  • If the generator yields a value: that yield is treated as a propagated failure and returned as-is.

Throws when the generator returns undefined without yielding, or yields more than once.


export function fromSafeTryAsync<T, E>(
gen: () => AsyncGenerator<IResultOfT<never, E>, T | undefined, unknown>,
): AsyncResult<T, E>;

Defined in: composition/safeTryAsync.ts

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

import { pipe } from '@sandlada/result/composition';
import { map, bind, match } from '@sandlada/result/operators';
import { ok, err } from '@sandlada/result/factories';
pipe(
ok(42),
map(x => x * 2),
bind(x => x > 50 ? ok(x) : err('too small')),
match(v => `OK: ${v}`, e => `Error: ${e}`),
); // "OK: 84"

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

import { pipeAsync } from '@sandlada/result/composition';
import { asyncOk, mapAsync, bindAsync, matchAsync, asyncErr } from '@sandlada/result/promise-result';
await pipeAsync(
asyncOk(42),
mapAsync(x => x * 2),
bindAsync(x => x > 50 ? asyncOk(x) : asyncErr('too small')),
matchAsync(v => `OK: ${v}`, e => `Error: ${e}`),
);

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 — fromSafeTry catches 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).

export function* safeTry<T, E>(
result: IResultOfT<T, E>,
): Generator<IResultOfT<never, E>, T | undefined, unknown>;

Defined in: composition/safeTry.ts

import { safeTry } from '@sandlada/result/composition';
import { ok } from '@sandlada/result/factories';
function* pipeline() {
const value = yield* safeTry(ok(42));
return value;
}

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).

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

import { safeTryAsync, fromSafeTryAsync } from '@sandlada/result/composition';
import { asyncOk, asyncErr } from '@sandlada/result/factories';
const ok = await fromSafeTryAsync(async function* () {
const a = yield* safeTryAsync(asyncOk(40));
const b = yield* safeTryAsync(asyncOk(2));
return Number(a) + Number(b);
}).run();
// Ok(42)
const failure = await fromSafeTryAsync(async function* () {
yield* safeTryAsync(asyncErr('boom'));
return 0;
}).run();
// Err('boom')