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);
...