TypeScript: explicit errors with a Result type
Return success or failure as a value, and let the compiler force the caller to handle both.
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
Explanation
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.
Explore the dry run
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 |
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.