ross@normalcool
2026-01-03

Treating errors as values in js/ts is the way.

export type TryCatchResult<T> =
  | readonly [result: T, error: null]
  | readonly [result: null, error: Error];

/**
 * Runs an async function and returns a discriminated union tuple:
 * - [result, null] on success
 * - [null, Error] on failure
 *
 * Notes:
 * - Preserves the original Error when possible; wraps non-Error throws.
 * - Readonly tuple prevents accidental mutation.
 * - Infers the type of the result.
 */
export async function tryCatch<T>(fn: () => Promise<T>): Promise<TryCatchResult<T>> {
  try {
    const result = await fn();
    return [result, null] as const;
  } catch (err) {
    const error = err instanceof Error ? err : new Error(String(err));
    return [null, error] as const;
  }
}

Example

import { tryCatch } from "./tryCatch.ts";

async function getData() {
  const res = await fetch("https://api.example.com/data");
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res
}

const [fetchRes, fetchErr] = await tryCatch(getData);

if (err) {
  console.error("Failed:", err);
} else {
  console.log("Success:", data.message);
}

const [resJson, resErr] = await tryCatch(fetchRes.json);
...