Skip to content

Getting started

Tutorial. A hands-on first lesson. Follow it top to bottom and you'll have written, chained, and folded your first Results. We keep explanation to a minimum here and link out to it — the goal is to do, not to study.

By the end you will have a small program that parses a value and handles every outcome as a value — with no try/catch to write. It takes about ten minutes.

Step 1 — Install

sh
pnpm add unthrown
sh
npm install unthrown
sh
yarn add unthrown

unthrown is ESM-first, ships dual CJS/ESM builds with full types, and has zero runtime dependencies — the exhaustive error matcher is built-in (exported as match / P). Use it with TypeScript in strict mode.

Step 2 — Return a failure instead of throwing

A Result<T, E> is either an Ok carrying a value T, or an Err carrying a modeled error E. E lists only the failures you anticipate.

Say you're parsing a user-supplied age. Two things can go wrong. Model both instead of throwing:

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

type AgeError = "not_a_number" | "negative";

function parseAge(input: string): Result<number, AgeError> {
  const n = Number(input);
  if (Number.isNaN(n)) return Err("not_a_number");
  if (n < 0) return Err("negative");
  return Ok(n);
}

parseAge("42"); // => Ok(42)
parseAge("-3"); // => Err("negative")
parseAge("x"); // => Err("not_a_number")

Nothing is thrown — both outcomes come back as values. Notice the signature now tells the whole truth: a caller can see that parseAge may fail, and how.

Step 3 — Transform and chain

Success combinators run only on Ok; an Err passes straight through, so you can chain without checking at every step:

ts
const adult = parseAge("42")
  .map((n) => n + 1) // => Ok(43)   — map: callback returns a plain value
  .flatMap((n) => (n >= 18 ? Ok(n) : Err("underage"))); // => Ok(43) — flatMap: callback returns a Result
// adult: Result<number, AgeError | "underage">  — flatMap unioned the error channels

The value stays wrapped in a Result the whole way — you extract it once, at the edge, in Step 4. Nothing is thrown along the way.

And when a step fails, the rest of the chain is skipped:

ts
const parsed = parseAge("x") // => Err("not_a_number")
  .map((n) => n + 1); // callback never runs — still Err("not_a_number")

if (parsed.isErr()) parsed.error; // => "not_a_number"

Rule of thumb: reach for map when your callback returns a plain value, flatMap when it returns another Result. (The full picture is in the combinator reference — you don't need it yet.)

Step 4 — Handle every outcome with match

At the edge of your program, fold a Result into a single value with match. You handle three runtime channels — ok, errCases, and a defect channel for the unexpected (you'll meet it in the next step):

ts
const message = parseAge("-3").match({
  ok: (age) => `age is ${age}`,
  // `errCases` receives an exhaustive matcher — one branch per error. It
  // matches plain strings too (no tag required), and a missing case won't compile.
  errCases: (matcher) =>
    matcher
      .with("negative", () => "must be positive")
      .with("not_a_number", () => "not a number"),
  defect: (cause) => {
    console.error(cause); // a bug slipped through — log it, don't leak it
    return "something went wrong";
  },
});
// => "must be positive"

Try deleting the .with("not_a_number", …) branch — your editor will flag it as a compile error. That's the point: the error channel is matched exhaustively, so a new failure can never be silently ignored.

Step 5 — Meet a defect

What happens if a callback throws by accident — a typo, a JSON.parse on bad input? unthrown catches it and turns it into a Defect, a third state that is not part of your error type:

ts
const result = parseAge("42").map((age) => {
  throw new Error("boom"); // an unexpected bug
});

result.isDefect(); // => true — not an Err, and not in AgeError

Because a thrown bug becomes a defect (never an Err), the single match from Step 4 needs no surrounding try/catch — the defect arm catches everything unexpected. That is the whole promise of the library: modeled failures travel as values, and bugs are quarantined in their own channel.

What you built

You now have a function that:

  • returns its failures as typed values instead of throwing;
  • chains transformations that skip automatically on failure;
  • folds every outcome — success, modeled error, and unexpected bug — in one exhaustive match.

Where to go next

Released under the MIT License.