TypeScript Results, Not Exceptions
A Result is a discriminated union with a success shape and a failure shape, told apart by the ok tag. A function returns a Result instead of throwing, so the caller must handle both shapes.
Returning a Result instead of throwing
divide returns a failure shape for a zero divisor and a success shape otherwise. The check on ok reads the value safely.
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
function divide(a: number, b: number): Result<number, string> {
if (b === 0) {
return { ok: false, error: 'Division by zero' };
}
return { ok: true, value: a / b };
}
const outcome = divide(10, 2);
if (outcome.ok) {
console.log(outcome.value);
} else {
console.log(outcome.error);
}Handling both branches
A switch on the ok tag handles the success input and the failure input. Each branch reads only the field its shape holds.
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
function parseWhole(text: string): Result<number, string> {
const value = Number(text);
if (Number.isNaN(value)) {
return { ok: false, error: 'Not a number: ' + text };
}
return { ok: true, value };
}
function showParsed(text: string): void {
const outcome = parseWhole(text);
switch (outcome.ok) {
case true:
console.log('Value: ' + outcome.value);
break;
case false:
console.log('Failed: ' + outcome.error);
break;
}
}
showParsed('42');
showParsed('abc');Example
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
function halfIfPositive(value: number): Result<number, string> {
if (value < 0) {
return { ok: false, error: 'Negative value' };
}
return { ok: true, value: value / 2 };
}
function showHalf(value: number): void {
const outcome = halfIfPositive(value);
if (outcome.ok) {
console.log('Half: ' + outcome.value);
} else {
console.log('Failed: ' + outcome.error);
}
}
showHalf(10);
showHalf(-4);Key points
- The success shape is { ok: true, value: T } and the failure shape is { ok: false, error: E }.
- Checking the ok tag narrows the union, so value exists only in the success branch and error only in the failure branch.
- A Result makes failure part of the return type, visible at every call site.
- Handling both branches covers the failure without a try block or a catch variable.
In this example, one function returns a Result for a good input and a bad one, and the caller prints a line for each branch of the union.
Note
Throwing still suits failures the caller cannot handle locally, while a Result suits failures the caller expects.
Try it Yourself
Edit the code, then press Run.
Running is simulated ยท TypeScript Playground
Exercise
How does a caller read the value of a Result safely?