Explicit errors as values for TypeScript — with a separate defect channel for the unexpected, and qualification enforced at every boundary.
Ordinary errors are unthrown — returned as values, not flung up the stack.
Only a true defect ever throws, and only at unwrap.
📖 Documentation · Why unthrown? · Getting Started
Most errors-as-values libraries model anticipated failures in Result<T, E>
but have no channel for the unexpected — a thrown TypeError, an un-triaged
promise rejection, a bug in a callback. Fold both into the same E and a bug
starts to look like a domain error.
unthrown keeps a third runtime state — a Defect — that is invisible to
the type. E lists only your anticipated errors; anything unexpected becomes a
defect that short-circuits to the edge, where you log it and return a 500.
- Errors as values.
map/flatMap/matchover aResult<T, E>. - A separate defect channel. Unmodeled failures can't masquerade as domain
errors, and can only be observed by
matchorrecoverDefect. - Qualification at every boundary.
fromPromise/fromThrowableforce you to triage each failure into a modeled error or a defect — no path yieldsunknowninE. - Small and done-able. Zero runtime dependencies, ESM-first, dual CJS/ESM, fully typed.
See Why unthrown? for
the comparison with neverthrow, boxed, and effect.
pnpm add unthrownimport { fromPromise, defect, TaggedError } from "unthrown";
class NotFound extends TaggedError("NotFound") {}
// Cross an async boundary — every rejection MUST be triaged into E or a defect.
const user = fromPromise(fetchUser(id), (cause) =>
cause instanceof NotFoundError ? new NotFound() : defect(cause),
);
// Handle every channel once, at the edge — no surrounding try/catch.
const status = await user.match({
ok: () => 200,
err: () => 404, // your modeled NotFound
defect: (cause) => {
logger.error(cause); // everything unexpected
return 500;
},
});A throw inside any combinator (.map, .flatMap, …) is caught and becomes a
defect, so the edge of your program needs a single match and no try/catch.
| Package | Description |
|---|---|
unthrown |
The core Result / AsyncResult, interop, TaggedError, matchTags. Zero runtime deps. |
@unthrown/vitest |
Vitest matchers: toBeOk, toBeOkWith, toBeErr, toBeErrTagged, toBeDefect. |
@unthrown/pattern |
Thin ts-pattern sugar for the natively-matchable Result: P.ok/P.err/P.defect, tag. |
@unthrown/effect |
Effect interop: Result ↔ Exit (bijection), Either, Effect. |
@unthrown/neverthrow |
neverthrow interop: Result ↔ Result, AsyncResult ↔ ResultAsync. |
@unthrown/boxed |
Boxed interop: Result ↔ Result, AsyncResult ↔ Future<Result>. |
This is a pnpm + turbo monorepo. Common tasks:
pnpm install
pnpm build # build all packages (tsdown, dual CJS/ESM)
pnpm test # run the Vitest suites
pnpm typecheck # tsc --noEmit across packages
pnpm lint # oxlint
pnpm format # oxfmtMIT © Benoit TRAVERS