Skip to contentJMRG
All posts

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 <|-- Err

TypeScript 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.