Stop Throwing Exceptions. Use Option and Result Instead.

Published: (February 20, 2026 at 07:10 AM EST)
3 min read
Source: Dev.to

Source: Dev.to

The Problem with JavaScript Error Handling

function getUser(id: number): User | null {
  // ...
}

The caller must remember to null‑check. The type system nudges them, but nothing prevents this:

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

A worse pattern is hiding possible failures in an async function:

async function fetchConfig(): Promise {
  // can throw network error, parse error, validation error...
  // none of these appear in the type signature
}

The errors are invisible, so the caller doesn’t know what to handle.

Introducing Option and Result in TypeScript

Rust’s Option and Result make the possibility of absence or failure explicit in the type signature. The same idea is brought to TypeScript by @rslike/std.

npm i @rslike/std

Working with Option

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

function findUser(id: number): Option {
  const user = db.find(u => u.id === id);
  return user ? Some(user) : None();
}

Safe Extraction

const opt = findUser(42);

// Fallback value
const user = opt.unwrapOr(guestUser);

Pattern Matching

const greeting = match(
  opt,
  (user) => `Hello, ${user.name}!`,
  () => "Hello, guest!"
);

Transformations

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

Chaining Options

const avatar = findUser(42)
  .flatMap(u => findAvatar(u.avatarId))
  .unwrapOr(defaultAvatar);

Basic API

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!

Working with Result

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

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

Inspecting the Result

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

Parsing JSON with Result

function parseJSON(raw: string): Result {
  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);

Matching on Result

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

The match Utility

match works with booleans, Option, and Result, providing exhaustive two‑branch dispatch.

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

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

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

// Result
match(someResult, (n) => n * 2, (e) => -1);

TypeScript infers the callback parameter types from the input, preventing accidental misuse of the error handler as the success handler.

Global Imports (Optional)

// 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"));

Benefits for Code Review

When Option and Result appear in function signatures, discussions shift from “Did we forget to handle a null/exception?” to “What is the intended contract?”

BeforeAfter
function getSession(token: string): Session | nullfunction getSession(token: string): Option
async function createOrder(cart: Cart): Promiseasync function createOrder(cart: Cart): Promise>

Installation

npm i @rslike/std
0 views
Back to Blog

Related posts

Read more »