Skip to content

Result Type

When to use

Use this pattern when a function can fail and you want to represent success or failure as a value rather than throwing an exception. The pattern makes error handling explicit at the call site, prevents unhandled exceptions from propagating, and works well for operations like parsing, validation, or I/O where failure is expected.

The pattern

A frozen Result class with two constructors — ok() for success and err() for failure — plus a try() helper that wraps a function call and catches any thrown error.

Basic usage

Result.try() wraps a function call in a try/catch and returns a Result instead of throwing. The caller inspects .ok to determine success or failure, and .value or .error to access the outcome:

javascript
function parseJSON(text) {
  return Result.try(() => JSON.parse(text));
}

const good = parseJSON('{"name": "Alice"}');
good.ok;    // true
good.value; // { name: "Alice" }
good.error; // undefined

const bad = parseJSON("not json");
bad.ok;    // false
bad.value; // undefined
bad.error; // SyntaxError: ...

Branching on result

Check .ok to branch on success or failure. On success, .value holds the return value; on failure, .error holds the caught exception:

javascript
const result = parseJSON(input);

if (result.ok) {
  process(result.value);
} else {
  console.error("Parse failed:", result.error.message);
}

Explicit construction

Use Result.ok() and Result.err() directly when the logic doesn't throw:

javascript
function validateAge(age) {
  if (typeof age !== "number" || age < 0) {
    return Result.err(
      new TypeError("age must be a non-negative number")
    );
  }
  return Result.ok(age);
}

Frozen instances

Every Result is frozen after construction. The ok, value, and error properties cannot be reassigned:

javascript
const r = Result.ok(42);
r.ok = false; // silently ignored (frozen)
r.ok;         // true

Implementing Result

The class freezes each instance after construction so that ok, value, and error are immutable. Result.ok() and Result.err() are static factories, and Result.try() wraps a throwable function in a try/catch that returns a Result instead of propagating the exception.

javascript
export class Result {
  constructor(ok, value, error) {
    Object.assign(this, {
      ok: Boolean(ok),
      value,
      error,
    });
    return Object.freeze(this);
  }
  
  static ok(value) {
    return new Result(true, value);
  }
  
  static err(error) {
    return new Result(false, undefined, error);
  }
  
  static try(fn) {
    try {
      return Result.ok(fn());
    } catch (error) {
      return Result.err(error);
    }
  }
}

Trade-offs

  • Pros: Makes error handling explicit — callers must check .ok before using .value. Frozen instances prevent accidental mutation. Result.try() wraps throwable code without manual try/catch at every call site.
  • Cons: Adds a wrapper object around every return value. Callers must remember to check .ok — unlike exceptions, nothing forces you to handle the error.
  • Versus throwing: Use Result when failure is a normal, expected outcome (parsing, validation, lookups). Use throw for truly exceptional conditions (programmer errors, invariant violations).
  • Versus nullable returns: Result carries the error context. Returning null loses the reason for failure, making debugging harder.