Skip to content

Instantly share code, notes, and snippets.

@bryanmylee
Last active October 13, 2025 15:11
Show Gist options
  • Select an option

  • Save bryanmylee/2403aedfd9abaf66612de41832de4ddc to your computer and use it in GitHub Desktop.

Select an option

Save bryanmylee/2403aedfd9abaf66612de41832de4ddc to your computer and use it in GitHub Desktop.
Type-safe and performant error handling in TypeScript
class Err<E> {
constructor(public error: E) {}
}
export function err<E>(err: E): Result<never, E> {
return new Err(err);
}
export function isErr<T, E>(value: Result<T, E>): value is Err<E> {
return value instanceof Err;
}
export type Result<T, E = unknown> = T | Err<E>;
/**
* Handle errors more declaratively without try catch.
*
* Non-async functions returning `T` will return `Result<T, unknown>`, and
* async functions returning `Promise<T>` will return
* `Promise<Result<T, unknown>>`.
*
* Supports overloaded function definitions. Refer to
* {@link OverloadedAttemptFn}.
*
* ```
* try {
* const data = doSomething(30);
* console.log(data);
* } catch (err) {
* console.log(err);
* }
* // becomes
* const data = attempt(doSomething)(30);
* if (!isErr(data)) {
* console.log(data);
* } else {
* console.log(data.err);
* }
* ```
*
* @param fn A throwable function to run.
* @returns A function that returns a Result instead of throwing an error.
*/
export function attempt<TFn extends CallableFunction>(
fn: TFn,
): OverloadedAttemptFn<TFn> {
// `as OverloadedResultFn<TFn>` required to overcome TypeScript's lack of
// higher-kinded types.
// eslint-disable-next-line @typescript-eslint/promise-function-async, @typescript-eslint/no-unsafe-type-assertion
return function (...args: unknown[]) {
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-assignment
const returnValue = fn(...args);
if (returnValue instanceof Promise) {
return returnValue.catch(err);
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return returnValue;
} catch (e) {
return err(e);
}
} as OverloadedAttemptFn<TFn>;
}
/**
* Due to a lack of higher-kinded types, we cannot capture an arbitrary number
* of overloads.
*
* Add as many overloaded types as needed.
*
* Internal use only.
*/
type OverloadedAttemptFn<TFn> = TFn extends {
(...args: infer TArgs1): infer TReturn1;
(...args: infer TArgs2): infer TReturn2;
}
? {
(...args: TArgs1): Attempt<TReturn1>;
(...args: TArgs2): Attempt<TReturn2>;
}
: never;
/**
* Determine the return type of `attempt` based on the inner function's return
* type.
*
* 1. If the inner function returns `any`, this returns `Result<any, unknown>`.
* 2. If the inner function returns a promise, this returns
* `Promise<Result<TAwaited>>`.
* 3. Otherwise, this returns `Result<TReturn>`.
*
* Internal use only.
*/
type Attempt<TReturn> =
IsStrictlyAny<TReturn> extends true
? Result<TReturn>
: TReturn extends Promise<infer TAwaited>
? Promise<Result<TAwaited>>
: Result<TReturn>;
// Returns true if type is any, or false for any other type.
type IsStrictlyAny<T> = (T extends never ? true : false) extends false
? false
: true;
@bryanmylee
Copy link
Author

bryanmylee commented Oct 8, 2025

I've tried multiple implementations of the Ok and Err variants.

This seems to be the most performant so far.

  1. Stopped using an object with ok: boolean to discriminate the Ok and Err variants. Instead, an Err class is used with a fast instanceof check. Consequently, the Ok variant can simply be the raw value without new allocation.
  2. Initially, Err extends Error was used to force the capture of stack traces, but this impacted performance significantly. Instead, if stack traces are desired, embed an error with err(new Error(message)).

Refer to this micro-benchmark.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment