Skip to content

btravstack/unthrown

Repository files navigation

unthrown

unthrown

Explicit errors as values for TypeScript — with a separate defect channel for the unexpected, and qualification enforced at every boundary.

CI npm license

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

Why?

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 / match over a Result<T, E>.
  • A separate defect channel. Unmodeled failures can't masquerade as domain errors, and can only be observed by match or recoverDefect.
  • Qualification at every boundary. fromPromise / fromThrowable force you to triage each failure into a modeled error or a defect — no path yields unknown in E.
  • 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.

Install

pnpm add unthrown

Example

import { 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.

Packages

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

Contributing

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       # oxfmt

License

MIT © Benoit TRAVERS

Packages

 
 
 

Contributors