TypeScript · Course navigation

TypeScript: explicit errors with a Result type

Return success or failure as a value, and let the compiler force the caller to handle both.

On this page
Before you start

TypeScript union types and narrowing.

  • Return success or failure as a value, and let the compiler force the caller to handle both.
  • Predict the sample output and explain the state changes.
  • Complete the practice task and check the boundary cases.

The concept

Exceptions can be thrown from anywhere and are invisible in a function's type, so callers easily forget to handle them. A Result type makes failure part of the signature. It is a discriminated union: either an ok result carrying a value, or a failed result carrying an error. The ok field is the discriminant.

After checking result.ok, TypeScript narrows the type, so result.value is available only in the success branch and result.error only in the failure branch. Reading the wrong one is a compile error. Using Result for expected failures, such as invalid user input, and exceptions for programming errors, keeps the code honest and easy to test.

Code example

TypeScript
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };

function parseAge(text: string): Result<number, string> {
  const n = Number(text);
  if (text.trim() === "" || Number.isNaN(n)) {
    return { ok: false, error: "not a number" };
  }
  if (!Number.isInteger(n) || n < 0 || n > 150) {
    return { ok: false, error: "out of range" };
  }
  return { ok: true, value: n };
}

for (const input of ["42", "abc", "200", ""]) {
  const result = parseAge(input);
  if (result.ok) {
    console.log("age " + result.value);
  } else {
    console.log("error: " + result.error);
  }
}
Output
age 42
error: not a number
error: out of range
error: not a number

Follow the code

Open the walkthrough to inspect the example one step at a time.

Show guided walkthrough
Guided walkthroughReady
1type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
2
3function parseAge(text: string): Result<number, string> {
4 const n = Number(text);
5 if (text.trim() === "" || Number.isNaN(n)) {
6 return { ok: false, error: "not a number" };
7 }
8 if (!Number.isInteger(n) || n < 0 || n > 150) {
9 return { ok: false, error: "out of range" };
10 }
11 return { ok: true, value: n };
12}
13
14for (const input of ["42", "abc", "200", ""]) {
15 const result = parseAge(input);
16 if (result.ok) {
17 console.log("age " + result.value);
18 } else {
19 console.log("error: " + result.error);
20 }
21}

Follow the example

Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.

View the complete dry-run table
Prepared trace of the published program. Some rows group repeated iterations.
Step / lineWhat happensState after stepOutput so far
A discriminated union: the ok field tells which shape the value has.—
—
Expected failures are returned, not thrown.—
—
Inside this branch result.value exists; in the else branch result.error exists.—
—
42 is valid.—
age 42
abc is not a number, 200 is out of range, and the empty text is rejected before conversion (Number('') is 0).—
age 42
error: not a number
error: out of range
error: not a number

Behind the scenes

How TypeScript processes this code

TypeScript checks types and emits JavaScript. Type annotations are removed before execution.

Cost and common mistakes

O(1) per call.

  • Without the check for empty text, Number('') would be 0 and an empty field would be accepted as the age 0.
  • Do not mix styles. If some paths return a Result and others throw, callers must guard against both.
  • A Result can be ignored if the caller never reads it. Lint rules can require handling.

Try it yourself

What would a caller see for the input 12.5?

Reveal answer and reasoning

error: out of range, because 12.5 is a number but not an integer.

Further reading