DEV Community

Cover image for Stop Throwing Exceptions. Use Option and Result Instead.
Vitali Haradkou
Vitali Haradkou

Posted on

Stop Throwing Exceptions. Use Option and Result Instead.

Let's talk about what's wrong with JavaScript error handling. Here's a function:

function getUser(id: number): User | null {
  // ...
}
Enter fullscreen mode Exit fullscreen mode

The caller has to remember to null-check. The type system nudges them, but there's nothing stopping this:

const user = getUser(42);
console.log(user.name); // TypeError at runtime if user is null
Enter fullscreen mode Exit fullscreen mode

Or this pattern, which is even worse:

async function fetchConfig(): Promise<Config> {
  // can throw network error, parse error, validation error...
  // none of these appear in the type signature
}
Enter fullscreen mode Exit fullscreen mode

The errors are invisible. The caller doesn't know what to handle.

There's a better model

Rust has Option<T> for values that might not exist, and Result<T, E> for operations that can fail. Both are explicit in the type signature. Both force the caller to handle every case.

@rslike/std brings this to TypeScript.

npm i @rslike/std
Enter fullscreen mode Exit fullscreen mode

Option — make absence impossible to ignore

import { Some, None, Option, match } from "@rslike/std";

function findUser(id: number): Option<User> {
  const user = db.find(u => u.id === id);
  return user ? Some(user) : None();
}
Enter fullscreen mode Exit fullscreen mode

Now the caller must handle both states. They can't accidentally treat None as a value:

const opt = findUser(42);

// Safe extraction with fallback
const user = opt.unwrapOr(guestUser);

// Pattern matching — handles both branches exhaustively
const greeting = match(
  opt,
  (user) => `Hello, ${user.name}!`,
  ()     => "Hello, guest!"
);
Enter fullscreen mode Exit fullscreen mode

Transform without unwrapping

const displayName = findUser(42)
  .map(u => `${u.firstName} ${u.lastName}`)
  .unwrapOr("Unknown User");

// Chain operations that also return Option
const avatar = findUser(42)
  .flatMap(u => findAvatar(u.avatarId))
  .unwrapOr(defaultAvatar);
Enter fullscreen mode Exit fullscreen mode

Quick checks

const opt = Some("hello");

opt.isSome();     // true
opt.isNone();     // false
opt.unwrap();     // "hello"

const empty = None();
empty.isNone();   // true
empty.unwrapOr("fallback"); // "fallback"
empty.unwrap();   // throws UndefinedBehaviorError — intentional!
Enter fullscreen mode Exit fullscreen mode

Result — make errors part of the contract

import { Ok, Err, Result, match } from "@rslike/std";

function divide(a: number, b: number): Result<number, string> {
  return new Result((ok, err) => {
    if (b === 0) {
      err("Division by zero");
    } else {
      ok(a / b);
    }
  });
}
Enter fullscreen mode Exit fullscreen mode

The error type is in the signature. Callers know what to expect:

const r = divide(10, 2);

r.isOk();   // true
r.unwrap();  // 5

const bad = divide(10, 0);
bad.isErr();             // true
bad.unwrapErr();         // "Division by zero"
bad.unwrapOr(0);         // 0
Enter fullscreen mode Exit fullscreen mode

Wrapping existing code that throws

function parseJSON(raw: string): Result<unknown, SyntaxError> {
  return new Result((ok, err) => {
    try {
      ok(JSON.parse(raw));
    } catch (e) {
      err(e as SyntaxError);
    }
  });
}

const config = parseJSON(rawInput)
  .map(data => validate(data))
  .mapErr(e => `Invalid config: ${e.message}`)
  .unwrapOr(defaults);
Enter fullscreen mode Exit fullscreen mode

Pattern matching

const message = match(
  parseJSON(rawInput),
  (data) => `Loaded: ${JSON.stringify(data)}`,
  (err)  => `Error: ${err.message}`
);
Enter fullscreen mode Exit fullscreen mode

match — exhaustive two-branch dispatch

match works with Option, Result, and boolean:

import { match } from "@rslike/std";

// boolean
match(isAdmin, (t) => "admin panel", (f) => "dashboard");

// Option<string>
match(someOption, (value) => `Got: ${value}`, () => "nothing");

// Result<number, Error>
match(someResult, (n) => n * 2, (e) => -1);
Enter fullscreen mode Exit fullscreen mode

TypeScript infers the callback parameter types from the input — you can't accidentally use the error handler as the success handler.

Globals — skip the imports

// entry.ts — once
import "@rslike/std/globals";

// anywhere else in your app — no imports needed
const x = Some(42);
const r = Ok("success");
const n = None();
const e = Err(new Error("oops"));
Enter fullscreen mode Exit fullscreen mode

The real benefit

When Option and Result are in your function signatures, code review becomes a conversation about intent rather than a hunt for unhandled edge cases.

// Before: what does null mean here? forgotten value? intentional absence?
function getSession(token: string): Session | null

// After: clear contract — either a session or nothing
function getSession(token: string): Option<Session>

// Before: can this throw? which errors?
async function createOrder(cart: Cart): Promise<Order>

// After: explicit failure type in the signature
async function createOrder(cart: Cart): Promise<Result<Order, OrderError>>
Enter fullscreen mode Exit fullscreen mode

Install

npm i @rslike/std
Enter fullscreen mode Exit fullscreen mode

Source: github.com/vitalics/rslike


Have you used Option/Result patterns in TypeScript before? What library are you using? Let me know below.

Top comments (0)