@sandlada/result is a TypeScript library implementing the Result Pattern — a type-safe, exception-free approach to error handling. It makes error flows explicit in the type system so you never wonder whether a function can fail.
Unlike traditional Result libraries that hardcode a single error type, @sandlada/result is fully generic: you bring your own error shapes (discriminated unions, classes, or plain objects).
- Fully generic
TError— define your own error types - Pure FP — data-last curried operators (
pipe,map,bind) with discriminated union types - Option type —
IOption<T>(Some / None) with curried operators - Async-native —
asyncOk/asyncErrfactories +pipeAsyncfor Promise-based railways, plus lazyAsyncResult/AsyncOptionthunks - Railway Oriented Programming built-in —
map,bind,orElse,match,tap,combine - Reliability — bounded
retry,timeout,race,any,allSettledfor production pipelines - Observability — breadcrumb
withPath/ctx/tapErrContext+format/inspect/installObserver - JSON serializable — result and option objects survive
JSON.stringify - Zero dependencies
- ESM-only, strict TypeScript
- Inspired by the C# Result Pattern and Rust's
Option<T>
npm i @sandlada/resultESM only. This package cannot be used with
require(). Your project must use ESM (import) or dynamicimport().
The main barrel @sandlada/result is type-focused. Its only runtime value is an empty default object, used to materialize the entry and sourcemap; functional runtime values come from dedicated subpath packages — pick the one that matches your shape.
import type { IResultOfT } from '@sandlada/result'; // type contracts
import { ok, err } from '@sandlada/result/factories'; // core constructors
import { map, unwrapOr } from '@sandlada/result/operators'; // sync operators
import { pipe } from '@sandlada/result/composition'; // pipe / composeK / safeTry
// Define your error type (discriminated union recommended)
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 = db.find(id);
if (!user) {
return err<AppError>({ kind: 'NotFound', id }) as IResultOfT<User, AppError>;
}
return ok(user);
}
// FP curried style
const name = pipe(
getUser('42'),
map(u => u.name),
unwrapOr('Unknown'),
);Why subpath imports?
@sandlada/resultexposes many types —IResultOfT,IOption,AsyncResult,AsyncOption. Names likemap,bind,matchexist for bothIResultOfTandIOption. The compiler can't disambiguate; the package layout does. Subpath imports make the type explicit at the call site and keep tree-shaking total. Seesrc/types/README.mdfor the rationale.
Each subpath has its own spec — a module README.md with the full export table linked to source files. Full type signatures and JSDoc live in the source.
| Export path | Contents | Spec |
|---|---|---|
@sandlada/result |
Type-focused barrel — IResult, IResultOfT, IOption, AsyncResult, AsyncOption, plus the runtime empty default marker. Functional runtime values must use a subpath. |
src/types/README.md |
@sandlada/result/factories |
Core constructors (ok, err, asyncOk, asyncErr, tryCatch, fromPromise, …). |
src/factories/README.md |
@sandlada/result/operators |
Sync operators on IResultOfT (map, bind, match, pipe, …). |
src/operators/README.md |
@sandlada/result/option |
Sync IOption<T> operators (ofSome, ofNone, map, bind, okOr, transpose, …). |
src/option/README.md |
@sandlada/result/async-result |
Lazy AsyncResult<T, E> thunk operators. |
src/async-result/README.md |
@sandlada/result/async-option |
Lazy AsyncOption<T> thunk operators. |
src/async-option/README.md |
@sandlada/result/promise-result |
Eager async operators on Promise<IResultOfT>. |
src/promise-result/README.md |
@sandlada/result/promise-option |
Eager async operators on Promise<IOption>. |
src/promise-option/README.md |
@sandlada/result/composition |
pipe, composeK, safeTry, pipeAsync, composeKAsync. |
src/composition/README.md |
@sandlada/result/adapters |
toOption, fromOption, switchFn, liftMap, tee, … |
src/adapters/README.md |
@sandlada/result/combine |
combine, combineWithAllErrors, all. |
src/combine/README.md |
@sandlada/result/reliability |
retry, retryLazy, timeout, race, any, allSettled. |
src/reliability/README.md |
@sandlada/result/observability |
ctx, withPath, format, inspect, installObserver, … |
src/observability/README.md |
@sandlada/result/primitives |
cond, condErr, sequence, reduce, partitionOption, lift. |
src/primitives/README.md |
@sandlada/result/types |
Same type contracts as the main barrel, plus its own empty default marker. Kept for backward compatibility. | src/types/README.md |
Bind your error type once and eliminate generic boilerplate:
// app-result.ts
import { ok, err } from '@sandlada/result/factories';
import type { IResultOfT } from '@sandlada/result';
import type { AppError } from './errors.js';
export type AppResult<T = void> = IResultOfT<T, AppError>;
export const AppResult = {
Success<T>(value?: T): AppResult<T> { return (value === undefined ? ok() : ok(value)) as unknown as AppResult<T>; },
Failure(error: AppError): AppResult<never> { return err(error) as unknown as AppResult<never>; },
} as const;// usage — no TError generic anywhere
function getUser(id: string): AppResult<User> {
if (!id) return AppResult.Failure({ kind: 'Validation', fields: { id: 'Required' } });
return AppResult.Success({ id, name: 'Alice' });
}- result.sandlada.com — published documentation: guides, behavior modes, and the generated API reference
- AGENTS.md — the module map: every subpath, its module
README.mdspec, and its behavior-matrix section - docs/behavior-modes.md — behavior modes: the fail-fast / accumulate / throw policy matrix for every API
MIT