Last active
October 13, 2025 15:11
-
-
Save bryanmylee/2403aedfd9abaf66612de41832de4ddc to your computer and use it in GitHub Desktop.
Type-safe and performant error handling in TypeScript
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I've tried multiple implementations of the
OkandErrvariants.This seems to be the most performant so far.
ok: booleanto discriminate theOkandErrvariants. Instead, anErrclass is used with a fastinstanceofcheck. Consequently, theOkvariant can simply be the raw value without new allocation.Err extends Errorwas used to force the capture of stack traces, but this impacted performance significantly. Instead, if stack traces are desired, embed an error witherr(new Error(message)).Refer to this micro-benchmark.