BeCuriouzLearn something new every day
Result Types: Making Errors Part of the Signature
💡 A Result type moves failure from an invisible thrown value into the function's signature, so the compiler forces every caller to handle it.
In TypeScript, a function's type tells you what it returns — but a thrown Error is invisible. parseUser(input: string): User looks total, yet it might blow up. Because throw isn't in the type, the compiler can't force callers to handle failure, and catch (e) gives you unknown. The Result pattern (also called Either) fixes this by making errors part of the return type: instead of throwing, you return the failure as a value.
Model it as a discriminated union — a tagged union where a literal ok field lets TypeScript narrow the branches:
type Result<T, E> =
| { ok: true; value: T }
| { ok: false; error: E };
function parseAge(s: string): Result<number, "empty" | "NaN"> {
if (s === "") return { ok: false, error: "empty" };
const n = Number(s);
return Number.isNaN(n)
? { ok: false, error: "NaN" }
: { ok: true, value: n };
}Now the caller cannot reach .value without first checking ok — the compiler narrows the union, so the happy path and the error path are both type-checked. The error type is "empty" | "NaN", not unknown: your failures are now typed errors you can exhaustively switch over, and adding a new error variant surfaces as a compile error everywhere it's unhandled.
const r = parseAge(input);
if (!r.ok) {
// r.error is "empty" | "NaN" here
return handle(r.error);
}
console.log(r.value + 1); // r.value: numberThe trade-off: Result shines for expected, recoverable failures (validation, parsing, network) where you want callers to decide. Keep throw for truly exceptional bugs — a broken invariant you can't sanely recover from. Libraries like neverthrow add .map/.andThen so you can chain Results without a pyramid of if (!r.ok) checks.