Effective Error Handling with Result Types in TypeScript
publish: April 7, 2024
modified: September 27, 2024
Elevate your TypeScript error handling with the powerful 'Result' type. Learn how to implement this functional programming concept for clearer, safer, and more composable code that explicitly manages success and failure states.
TypeScript, while powerful, lacks a built-in construct specifically designed for function-level error handling. The standard Error
type, while useful, doesn’t quite fit the bill for representing potential failures in a type-safe manner. Let’s explore how we can fill this gap with a custom Result
type.
The Result Type
The Result
type encapsulates the outcome of a function that may succeed or fail. It adheres to two key principles:
- It can unwrap the underlying value.
- It allows building upon the underlying value.
Here’s a straightforward implementation:
enum ResultKind {
OK = "Ok",
ERR = "Err",
}
export type Result<T, E> = Ok<T> | Err<E>;
interface ResultBase<A, E> {
kind: ResultKind;
map<B>(fn: (_: A) => B): Result<B, E>;
bind<B>(fn: (_: A) => Result<B, E>): Result<B, E>;
match<B>(obj: { ok: (_: A) => B; err: (_: E) => B }): B;
}
export type Ok<A> = Readonly<ResultBase<A, never> & { kind: ResultKind.OK; value: A }>;
export function ok<A>(a: A): Ok<A> {
return {
kind: ResultKind.OK,
value: a,
map(fn) {
return ok(fn(this.value));
},
bind(fn) {
return fn(this.value);
},
match({ ok }) {
return ok(this.value);
},
};
}
export type Err<E> = Readonly<ResultBase<never, E> & { kind: ResultKind.ERR; error: E }>;
export function err<E>(e: E): Err<E> {
return {
kind: ResultKind.ERR,
error: e,
map() {
return this;
},
bind() {
return this;
},
match({ err }) {
return err(this.error);
},
};
}
Using the Result Type
Here’s how you might use the Result
type in practice:
function divide(a: number, b: number): Result<number, string> {
return b === 0 ? err("Division by zero") : ok(a / b);
}
// Example usage
const result1 = divide(10, 2);
const result2 = divide(8, 0);
// Pattern matching
result1.match({
ok: (value) => console.log(`Result: ${value}`),
err: (error) => console.error(`Error: ${error}`),
});
result2.match({
ok: (value) => console.log(`Result: ${value}`),
err: (error) => console.error(`Error: ${error}`),
});
Benefits of the Result Type
This implementation shares some properties with monads, though it’s a simplified version. The main advantages are:
- Improved function clarity: The return type explicitly indicates that an error might occur.
- Type safety: TypeScript ensures that both success and error cases are handled.
- Composability: Results can be chained together easily.
While it would be ideal to have such functionality at the language level, this pattern provides a workable solution for more robust error handling in TypeScript applications.
Conclusion
The Result
type offers a pragmatic approach to error handling in TypeScript. By adopting this pattern, you can write more predictable and maintainable code. Consider incorporating it into your projects and see how it impacts your error management strategy.
While I encourage creating this pattern in your own projects, there are numerous projects that have amazing API’s like (ts-results)[https://github.com/vultix/ts-results] and