Result Pattern: Elegant Error Handling without Exceptions
design-patternsbehavioralresult-pattern

Intent
The Result Pattern represents the result of an operation that can fail, encapsulating both success and error in a unified type. It eliminates the need to throw exceptions for control flow.
The Problem
Exceptions have issues:
// What exceptions can this function throw?
function processPayment(amount: number): PaymentResult {
// ValidationError? NetworkError? InsufficientFundsError?
// The signature doesn't say...
}
try {
const result = processPayment(100);
} catch (e) {
// What type is e? unknown...
}The Solution: Result Type
classDiagram
class Result~T, E~ {
<<interface>>
+isOk(): boolean
+isErr(): boolean
+unwrap(): T
+map~U~(fn): Result~U, E~
+andThen~U~(fn): Result~U, E~
+match~R~(handlers): R
}
class Ok~T~ {
-value: T
}
class Err~E~ {
-error: E
}
Result <|-- Ok
Result <|-- ErrTypeScript Implementation
type Result<T, E> = Ok<T, E> | Err<T, E>;
class Ok<T, E = never> {
readonly _tag = 'Ok';
constructor(readonly value: T) {}
isOk(): this is Ok<T, E> { return true; }
isErr(): this is Err<T, E> { return false; }
unwrap(): T { return this.value; }
unwrapOr(_default: T): T { return this.value; }
map<U>(fn: (value: T) => U): Result<U, E> {
return ok(fn(this.value));
}
andThen<U>(fn: (value: T) => Result<U, E>): Result<U, E> {
return fn(this.value);
}
}
class Err<T, E> {
readonly _tag = 'Err';
constructor(readonly error: E) {}
isOk(): this is Ok<T, E> { return false; }
isErr(): this is Err<T, E> { return true; }
unwrap(): T { throw new Error('Called unwrap on Err'); }
unwrapOr(defaultValue: T): T { return defaultValue; }
map<U>(_fn: (value: T) => U): Result<U, E> {
return err(this.error);
}
andThen<U>(_fn: (value: T) => Result<U, E>): Result<U, E> {
return err(this.error);
}
}
const ok = <T, E = never>(value: T): Result<T, E> => new Ok(value);
const err = <T = never, E = Error>(error: E): Result<T, E> => new Err(error);Practical Example
type ValidationError = { type: 'validation'; field: string; message: string };
function validateEmail(email: string): Result<string, ValidationError> {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return err({ type: 'validation', field: 'email', message: 'Invalid' });
}
return ok(email.toLowerCase().trim());
}
// Composition
const result = validateEmail(input)
.andThen(email => checkNotExists(email))
.map(email => ({ email, verified: false }));
// Pattern matching
const message = result.match({
ok: user => `Created: ${user.email}`,
err: error => `Error: ${error.message}`,
});When to Use
| Situation | Recommendation |
|---|---|
| Validations and parsing | ✅ Recommended |
| APIs with typed errors | ✅ Recommended |
| Operation composition | ✅ Recommended |
| Truly exceptional errors | ❌ Use exceptions |
Conclusion
The Result Pattern makes error paths explicit, facilitates composition, and eliminates scattered try/catch blocks.