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
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
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);
}
}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
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); }}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
| Step / line | What happens | State after step | Output 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.