Skip to content

Getting Started

@sandlada/result is a TypeScript library implementing the Result Pattern — a type-safe, exception-free approach to error handling. Success and failure both travel as plain objects, so the type system tells you when a call can fail instead of a try/catch block you have to remember to write.

Unlike traditional Result libraries that hardcode a single error type, this one is fully generic over TError: you bring your own error shapes.

Terminal window
npm i @sandlada/result
import type { IResultOfT } from '@sandlada/result';
import { ok, err } from '@sandlada/result/factories';
import { map, unwrapOr } from '@sandlada/result/operators';
import { pipe } from '@sandlada/result/composition';
type User = { id: string; name: string };
// Bring your own error type — a discriminated union works best.
type AppError =
| { kind: 'NotFound'; id: string }
| { kind: 'Validation'; fields: Record<string, string> };
function getUser(id: string): IResultOfT<User, AppError> {
if (!id) {
return err<AppError>({ kind: 'Validation', fields: { id: 'Required' } }) as IResultOfT<User, AppError>;
}
const user = users.get(id);
if (!user) {
return err<AppError>({ kind: 'NotFound', id }) as IResultOfT<User, AppError>;
}
return ok(user);
}
const users = new Map<string, User>([['42', { id: '42', name: 'Alice' }]]);
const name = pipe(
getUser('42'),
map((user) => user.name),
unwrapOr('Unknown'),
);

name is 'Alice'. Notice that nothing here can throw for a predictable failure: NotFound and Validation are values in the error channel.

The main barrel is type-focused. Its only runtime export is an empty default object, which exists so the entry and its sourcemap are materialized. Runtime values come from the subpath that matches the shape you are working with.

Import path Contents
@sandlada/result Type contracts (IResult, IResultOfT, IOption, AsyncResult, AsyncOption) and the empty default marker.
@sandlada/result/types The same type contracts, kept for backward compatibility.
@sandlada/result/factories ok, err, asyncOk, asyncErr, tryCatch, fromPromise, and friends.
@sandlada/result/operators Synchronous operators on IResultOfT: map, bind, match, unwrapOr, …
@sandlada/result/option Synchronous IOption<T> operators: ofSome, ofNone, map, bind, okOr, …
@sandlada/result/async-result Lazy AsyncResult<T, E> thunk operators.
@sandlada/result/async-option Lazy AsyncOption<T> thunk operators.
@sandlada/result/promise-result Eager operators on Promise<IResultOfT>.
@sandlada/result/promise-option Eager operators on Promise<IOption>.
@sandlada/result/composition pipe, composeK, safeTry, pipeAsync, composeKAsync.
@sandlada/result/adapters toOption, fromOption, switchFn, liftMap, tee, …
@sandlada/result/combine combine, combineWithAllErrors, all.
@sandlada/result/reliability retry, retryLazy, timeout, race, any, allSettled.
@sandlada/result/observability ctx, withPath, format, inspect, installObserver, …
@sandlada/result/primitives cond, condErr, sequence, reduce, partitionOption, lift.

Names like map, bind, and match exist for both IResultOfT and IOption. A single barrel cannot export two functions with the same name, so the package layout mirrors the type space instead: the import path tells both the compiler and the reader which shape is in play, and tree-shaking stays total.

Access value or error only after narrowing. Because the variants are a discriminated union, the compiler rejects the wrong access.

import { ok } from '@sandlada/result/factories';
import { unwrapOr } from '@sandlada/result/operators';
const result = ok(21);
if (result.isSuccess) {
// `value` exists here
console.log(result.value * 2);
} else {
// `error` exists here
console.error(result.error);
}
console.log(unwrapOr(0)(result));
  • Behavior modes — which APIs throw, which capture, and which short-circuit.
  • API Reference — every export, generated from the source JSDoc.