Introduction ​
INFO
This is an introduction to the Result type concept itself. For package documentation, see the Getting Started guide.
What is a Result Type? ​
A Result type is a powerful concept from functional programming that represents the outcome of an operation that can either succeed or fail. Think of it as a type-safe container that holds either a successful value or an error — never both, and never neither.
type Result<Data, Error> =
| { success: true; data: Data }
| { success: false; error: Error };
Instead of throwing exceptions or returning null
/undefined
for errors, Result types make success and failure explicit parts of your type system, forcing you to handle both cases.
Why Use Result Types? ​
The Result type embraces the "errors as values" philosophy, treating errors as data rather than exceptional control flow. Let's explore this concept through a simple division function to see how Result types improve error handling.
Errors Visible in Type Signatures ​
Traditional approach:
Traditional functions often hide potential errors in their type signatures. When you see a function like divide(a: number, b: number): number
, TypeScript tells you it returns a number, but it doesn't tell you that it might throw an exception.
function divide(a: number, b: number) {
if (b === 0) {
// Hidden error - not visible in type signature
throw new Error('division_by_zero');
}
return a / b;
}
// TypeScript says this returns `number`, but it can actually throw
const result = divide(10, 0); // đź’Ą Runtime exception
Result type approach:
With Result types, potential errors become part of the function's return type signature, making failures explicit and discoverable.
import { err, ok } from '@uni-ts/result';
function safeDivide(a: number, b: number) {
return b === 0 ? err('division_by_zero') : ok(a / b);
}
// TypeScript correctly shows it can return a number or an error.
const result = safeDivide(10, 0);
Predictable Control Flow ​
Now let's see what happens when we use these functions in a larger calculation.
Traditional approach:
With traditional exceptions, your program flow can be interrupted at any point, making it hard to reason about what will actually execute.
function calculate() {
const x = divide(10, 2); // 5 - works fine
const y = divide(8, 0); // đź’Ą Throws exception, disrupts the flow
return { x, y }; // This line never executes
}
Result type approach:
Result types give you predictable, linear control flow where errors are handled explicitly at each step.
function calculate() {
const x = safeDivide(10, 2); // Ok<number>
const y = safeDivide(8, 0); // Err<'division_by_zero'>
// No exceptions thrown, no flow interruption
return { x, y };
}
Required Error Handling ​
Returning x
and y
doesn't actually calculate anything. Let's return a sum of those numbers.
Traditional approach:
In the traditional approach, you don't see the errors until runtime, so you may forget to handle them.
function calculate() {
// Easy to forget error handling - code compiles fine
const x = divide(10, 2);
const y = divide(8, 0); // đź’Ą Runtime exception
return x + y; // This line will never execute
}
Result type approach:
With Result types, TypeScript will prevent you from accessing the data until you ensure no error has occurred.
function calculate() {
const xResult = safeDivide(10, 2);
const yResult = safeDivide(8, 0);
// We cannot access data without checking for errors
return xResult.data + yResult.data;}
function calculate() {
const xResult = safeDivide(10, 2);
const yResult = safeDivide(8, 0);
// Handle errors first
if (isErr(xResult) || isErr(yResult)) {
return 0;
}
// Then safely access the data
return xResult.data + yResult.data;
}
Explicit Error Information ​
Last but not least, let's handle possible errors from the divide
function to see how it will look like in both approaches.
Traditional approach:
The traditional approach requires you to dig into implementation details or use instanceof
checks to ensure you handle only the divide
function exceptions (you may place more throwable functions in the try
block in the future).
function calculate() {
try {
const x = divide(10, 2);
const y = divide(8, 0);
// ... maybe some more functions
return x + y;
} catch (ex) {
// What exception? From which function?
// We need to inspect implementation or use instanceof checks
// to ensure we handle only the `divide` function exceptions
if (ex instanceof Error && ex.message === 'division_by_zero') {
return 0;
}
// Re-throw in case of exceptions from other functions
// (current ones or introduced in the future)
throw ex;
}
}
Result type approach:
With Result types, nothing changes from the previous example. You are already sure you're handling only errors from the divide
function. If you introduce more functions in the future, they will come with their own Result return types, so TypeScript will tell you if they can fail and how.
function calculate() {
const xResult = safeDivide(10, 2);
const yResult = safeDivide(8, 0);
// We're sure errors come from the `safeDivide` function
if (isErr(xResult) || isErr(yResult)) {
return 0;
}
// ... maybe some more functions
// TypeScript will tell us if they can fail and how
return xResult.data + yResult.data;
}
Getting Started ​
Ready to start using Result types in your project? Check out the Getting Started guide to learn how to install and use the @uni-ts/result
package with practical examples and API documentation.