No more uncaught errors!
Evercatch is a tiny, dependency-free TypeScript library that turns thrown errors into values. Errors become part of a function's return type, so the compiler tells you where they are and refuses to let you read a value you haven't checked for yet.
npm install evercatch
# or
yarn add evercatch
# or
pnpm add evercatch
Everything is built on one type: a readonly tuple of [error, value].
type Result<T, E> = readonly [null, T] | readonly [E, null];
Destructure it, check the error, and TypeScript narrows the value for you:
import { err, ok, type Result } from "evercatch";
function divide(a: number, b: number): Result<number, Error> {
if (b === 0) {
return err(new Error("Division by zero"));
}
return ok(a / b);
}
const [error, value] = divide(10, 2);
if (error) {
console.error(error.message);
} else {
console.log(value); // number — narrowed, not number | null
}
null in the error slot means "this result is ok", so an error can never be
nullish. Every error type in the library is constrained to reject null and
undefined at compile time.
Wrap a call that might throw and get a result back instead.
import { fromPromise, resultFrom } from "evercatch";
const [parseError, config] = resultFrom(() => JSON.parse(raw));
const [fetchError, response] = await fromPromise(
fetch("https://api.example.com/data"),
);
Or wrap the function once and reuse the safe version:
import { fromAsyncThrowable, fromThrowable } from "evercatch";
const safeParse = fromThrowable(JSON.parse);
const safeFetch = fromAsyncThrowable(fetch);
const [error, data] = safeParse(raw);
const [fetchError, response] = await safeFetch("https://api.example.com/data");
Anything thrown that isn't an Error is wrapped in one, with the original value
kept as cause.
Every catching function takes an optional mapErr to turn the caught value into
an error type of your choosing — a string union, a tagged object, your own error
class. Whatever you return becomes the error type of the result.
import { fromPromise } from "evercatch";
type FetchError = "NETWORK_ERROR" | "TIMEOUT";
const [error, response] = await fromPromise(
fetch("https://api.example.com/data"),
(e): FetchError => (e instanceof DOMException ? "TIMEOUT" : "NETWORK_ERROR"),
);
if (error === "TIMEOUT") {
// ...
}
When you'd rather not handle the error at the call site, unwrap the result with a fallback — or throw after all.
import { unwrapOr, unwrapOrElse, unwrapOrThrow } from "evercatch";
unwrapOr(divide(10, 0), 0); // 0
unwrapOrElse(divide(10, 0), (error) => error.message.length); // computed
unwrapOrThrow(divide(10, 0)); // throws the error
The async variants take a Promise of a result and return a promise:
unwrapAsyncOr, unwrapAsyncOrElse and unwrapAsyncOrThrow.
Results compose by returning early. Errors travel upward as values, so a function that can fail has a signature that says so.
import { err, fromPromise, ok, type ResultAsync } from "evercatch";
import { auth } from "./auth";
async function fetchUserData(): ResultAsync<UserData, Error> {
const [authError, user] = await fromPromise(auth());
if (authError) {
return err(authError);
}
const [fetchError, response] = await fromPromise(
fetch(`https://api.example.com/user/${user.id}`),
);
if (fetchError) {
return err(fetchError);
}
if (!response.ok) {
return err(new Error("Failed to fetch user data"));
}
return await fromPromise(response.json());
}
The same functions are also grouped under the type they work with, which makes for shorter names at the call site. This is purely a matter of preference — the namespace members and the standalone exports are the same functions.
import { Result, ResultAsync, ResultAsyncFn, ResultFn } from "evercatch";
Result.ok(42);
Result.from(() => JSON.parse(raw));
Result.unwrapOr(someResult, 0);
await ResultAsync.from(fetch(url));
const safeParse = ResultFn.from(JSON.parse);
const safeFetch = ResultAsyncFn.from(fetch);
Note that Result, ResultAsync, ResultFn and ResultAsyncFn are each both a
type and a value, so a single import gives you both.
Full API reference: fransek.github.io/evercatch
MIT