Errors as values
Ordinary errors are returned in a Result
A small, focused Result type with a separate defect channel for the unexpected — and qualification enforced at every boundary.
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.