Skip to content

unthrownExplicit errors as values

A small, focused Result type with a separate defect channel for the unexpected — and qualification enforced at every boundary.

unthrownunthrown

At a glance

ts
import { Ok, Err, fromPromise, P, TaggedError, type Result } from "unthrown";

class NotFound extends TaggedError("NotFound") {}

function findUser(id: string): Result<User, NotFound> {
  const user = users.get(id);
  return user ? Ok(user) : Err(new NotFound());
}

// Cross an async boundary — every rejection MUST be triaged.
// (db.loadProfile rejects with MissingRowError when there is no row.)
const profile = fromPromise(db.loadProfile(id), (cause, defect) =>
  cause instanceof MissingRowError ? new NotFound() : defect(cause),
);

// Handle every channel once, at the edge — no surrounding try/catch.
const status = await profile.match({
  ok: () => 200,
  errCases: (matcher) => matcher.with(P.tag("NotFound"), () => 404), // every case, named
  defect: () => 500,
});

Ordinary errors travel as values through map / flatMap / match. A thrown bug becomes a defect that short-circuits to the edge — never silently folded into your domain errors.

errCases takes an exhaustive matcher: every case in E is named, so the day you add one, this call site stops compiling until you decide what it maps to.

Used in production at

Released under the MIT License.