https://github.com/btravstack/unthrown
Explicit errors as values for TypeScript — with a separate defect channel for the unexpected and qualification enforced at every boundary.
https://github.com/btravstack/unthrown
async defect either error-handling errors-as-values esm functional-programming monad railway-oriented-programming result result-type tagged-error type-safe typescript zero-dependencies
Last synced: about 7 hours ago
JSON representation
Explicit errors as values for TypeScript — with a separate defect channel for the unexpected and qualification enforced at every boundary.
- Host: GitHub
- URL: https://github.com/btravstack/unthrown
- Owner: btravstack
- License: mit
- Created: 2026-06-25T09:36:31.000Z (5 days ago)
- Default Branch: main
- Last Pushed: 2026-06-26T16:27:00.000Z (4 days ago)
- Last Synced: 2026-06-27T02:18:32.947Z (3 days ago)
- Topics: async, defect, either, error-handling, errors-as-values, esm, functional-programming, monad, railway-oriented-programming, result, result-type, tagged-error, type-safe, typescript, zero-dependencies
- Language: TypeScript
- Homepage: https://btravstack.github.io/unthrown/
- Size: 321 KB
- Stars: 4
- Watchers: 1
- Forks: 0
- Open Issues: 2
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
unthrown
> Explicit errors as values for TypeScript — with a separate defect channel for
> the unexpected, and qualification enforced at every boundary.
[](https://github.com/btravstack/unthrown/actions/workflows/ci.yml)
[](https://www.npmjs.com/package/unthrown)
[](./LICENSE)
Ordinary errors are _unthrown_ — returned as values, not flung up the stack.
Only a true defect ever throws, and only at `unwrap`.
📖 **[Documentation](https://btravstack.github.io/unthrown/)** ·
[Why unthrown?](https://btravstack.github.io/unthrown/guide/why-unthrown) ·
[Getting Started](https://btravstack.github.io/unthrown/guide/getting-started)
## Why?
Most errors-as-values libraries model _anticipated_ failures in `Result`
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`.
- **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?](https://btravstack.github.io/unthrown/guide/why-unthrown) for
the comparison with `neverthrow`, `boxed`, and `effect`.
## Install
```sh
pnpm add unthrown
```
## Example
```ts
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`](./packages/core) | The core `Result` / `AsyncResult`, interop, `TaggedError`, `matchTags`. Zero runtime deps. |
| [`@unthrown/vitest`](./packages/vitest) | Vitest matchers: `toBeOk`, `toBeOkWith`, `toBeErr`, `toBeErrTagged`, `toBeDefect`. |
| [`@unthrown/pattern`](./packages/pattern) | Thin `ts-pattern` sugar for the natively-matchable `Result`: `P.Ok`/`P.Err`/`P.Defect`, `tag`. |
| [`@unthrown/effect`](./packages/effect) | Effect interop: `Result ↔ Exit` (bijection), `Either`, `Effect`. |
| [`@unthrown/neverthrow`](./packages/neverthrow) | neverthrow interop: `Result ↔ Result`, `AsyncResult ↔ ResultAsync`. |
| [`@unthrown/boxed`](./packages/boxed) | Boxed interop: `Result ↔ Result`, `AsyncResult ↔ Future`. |
## Contributing
This is a pnpm + turbo monorepo. Common tasks:
```sh
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](./LICENSE) © Benoit TRAVERS