Skip to content

Instantly share code, notes, and snippets.

@t3dotgg
Last active March 3, 2025 06:52
Show Gist options
  • Save t3dotgg/a486c4ae66d32bf17c09c73609dacc5b to your computer and use it in GitHub Desktop.
Save t3dotgg/a486c4ae66d32bf17c09c73609dacc5b to your computer and use it in GitHub Desktop.
Theo's preferred way of handling try/catch in TypeScript
// Types for the result object with discriminated union
type Success<T> = {
data: T;
error: null;
};
type Failure<E> = {
data: null;
error: E;
};
type Result<T, E = Error> = Success<T> | Failure<E>;
// Main wrapper function
export async function tryCatch<T, E = Error>(
promise: Promise<T>,
): Promise<Result<T, E>> {
try {
const data = await promise;
return { data, error: null };
} catch (error) {
return { data: null, error: error as E };
}
}
@dchenk
Copy link

dchenk commented Feb 24, 2025

The as E assertion is not sound.

@tolly-xyz
Copy link

@dchenk can you elaborate?

@UdaraWanasinghe
Copy link

// Types for the result object with discriminated union
type Success<T> = {
  data: T;
  error?: never;
};

type Failure<E> = {
  data?: never;
  error: E;
};

type Result<T, E = Error> = Success<T> | Failure<E>;

// Main wrapper function
export async function tryCatch<T, E = Error>(
  promise: Promise<T>,
): Promise<Result<T, E>> {
  try {
    const data = await promise;
    return { data };
  } catch (error) {
    return { error: error as E };
  }
}

@IgorKhomenko
Copy link

Can it simply be

const response = await somePromise().catch((e) => ({ error: e }));

if (response.error) {
   // handle error
   return;
}

// handle success 
response.data

?

@alireza-ahmadi
Copy link

@EverStarck this is the way 💯

@Ebrahim-Ramadan
Copy link

what about:

// Discriminated union thingy for the result (who even reads this?)
type Ssssucccccessssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss<T> = { data: T; error: null; };
type Ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffailure<E = any> = { data: null; error: E; };
type RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR<T, E = unknown> =
  Ssssucccccessssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss<T> |
  Ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffailure<E>;

// Some wrapper thing I guess?
export async function tRy_CatcH_mE_If_YoU_cAn<T, E = unknown>(
  pRoMiSe: Promise<T>,
): Promise<RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR<T, E>> {
  try {
    const DaTaDooDah = await pRoMiSe;
    return { data: DaTaDooDah, error: null } as Ssssucccccessssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss<T>;
  } catch (OOPSIE_WHOOPSIE) {
    return {
      data: null,
      error:
        OOPSIE_WHOOPSIE instanceof Error
          ? (OOPSIE_WHOOPSIE as E)
          : (new Error("OH NOOOO: " + String(OOPSIE_WHOOPSIE)) as E),
    } as Ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffailure<E>;
  }
}

WhatsApp Image 2025-02-24 at 14 51 31_632c9ed1

@Ebrahim-Ramadan
Copy link

Can it simply be

const response = await somePromise().catch((e) => ({ error: e }));

if (response.error) {
   // handle error
   return;
}

// handle success 
response.data

?

yes it would but the type of response would not be strictly defined., we'd need to rely on the presence or absence of the error property to infer the state, thus the success/failure state is implicitly determined by the presence or absence of the error property yk

@sohomdatta1
Copy link

Why are we reinvented Golang in Typescript lol?

@emilien-jegou
Copy link

@t3dotgg My problem with this implementation is that this sort of error will still pass through:

const someFunction = (): Promise<number> => { throw 'uncaught'; return Promise.resolve(123) }
const res = await tryCatch(someFunction() /* throws here */)

I like neverthrow-ts way of handling this but you could check my minimal approach here:

// run code in a 'failable' context where thrown exception are converted to a result error
// e.g. failable(() => { throw 'uncaught'; return 123 })
export const failable = <const T>(cb: () => T): Result<T, unknown> => {
  try {
    return success(cb());
  } catch (e: unknown) {
    return failure<unknown>(e);
  }
};

// e.g. await failableAsync(() => { throw 'uncaught'; return Promise.resolve(123) })
export const failableAsync = async <const T>(cb: () => Promise<T>): Promise<Result<T, unknown>> => {
  try {
    return success(await cb());
  } catch (e: unknown) {
    return failure<unknown>(e);
  }
};

for example above you would just do: const res = await failableAsync(someFunction)

@tolly-xyz
Copy link

@emilien-jegou great point! i like your idea, but as written it doesnt allow for passing args. converting it to a decorator would be pretty clean imo

@emilien-jegou
Copy link

@tolly-xyz

You can still pass args using a nested call failableAsync(() => someFunction(/* whatever args here */)),

there's also a wrapper function in the gist that could help:

const myFunction = (t: boolean): number => {
  if (t) { throw 'invalid' }
 else { return 123 }
}
const mySafeFunction = safeify(myFunction)
mySafeFunction(false) // return `Success<number>`
mySafeFunction(true) // return `Error<unknown>`

@tolly-xyz
Copy link

You can still pass args using a nested call failableAsync(() => someFunction(/* whatever args here */)),

oh right duh, i need more coffee lol

@tanishqmanuja
Copy link

@rafifos
Copy link

rafifos commented Feb 24, 2025

i've been using this utility in my projects:

utils/promises.ts

/**
 * Handles a promise and returns an object indicating the success or failure of the promise.
 *
 * This function is inspired by the ECMAScript Safe Assignment Operator Proposal and the ECMAScript Try Operator.
 *
 * @template T - The type of the value that the promise resolves to.
 * @param {Promise<T>} promise - The promise to handle.
 * @returns {Promise<{ ok: boolean; value: T | null; error: Error | null }>}
 * An object containing:
 * - `ok`: A boolean indicating whether the promise was resolved successfully.
 * - `value`: The resolved value of the promise if successful, otherwise `null`.
 * - `error`: The error object if the promise was rejected, otherwise `null`.
 * @see {@link https://jsdev.space/ts-error-handling|TypeScript Error Handling: The Shift to Result Types}
 * @see {@link https://github.com/arthurfiorette/proposal-try-operator/tree/proposal-safe-assignment-operator|ECMAScript Safe Assignment Operator Proposal}
 * @see {@link https://github.com/arthurfiorette/proposal-try-operator|ECMAScript Try Operator}
 */
async function handleAsync<T>(promise: Promise<T>): Promise<{
  ok: boolean;
  value: T | null;
  error: Error | null;
}> {
  try {
    const data = await promise;

    return {
      ok: true,
      value: data,
      error: null,
    };
  } catch (error) {
    return {
      ok: false,
      value: null,
      error: error instanceof Error ? error : new Error(String(error)),
    };
  }
}

export { handleAsync };

@psnehanshu
Copy link

It won't work on this function, wlll it?

function example(): Promise<string> {
  throw new Error();
  return new Promise(r => r('hello world'));
}

@Todomir
Copy link

Todomir commented Feb 25, 2025

@psnehanshu thats because the return is actually unreachable, so the function will just throw synchronously

@kvnzrch
Copy link

kvnzrch commented Feb 25, 2025

Make it short and easy to understand

type Result<T, E = Error> = { data: T; error: null } | { data: null; error: E };

export async function tryCatch<T, E = Error>(promise: Promise<T>): Promise<Result<T, E>> {
  try {
    return { data: await promise, error: null };
  } catch (error) {
    return { data: null, error: error as E };
  }
}

@psnehanshu
Copy link

psnehanshu commented Feb 25, 2025

@psnehanshu thats because the return is actually unreachable, so the function will just throw synchronously

@Todomir Yes, the point being, unless you know how the function is written, tryCatch can't guarantee it will always work as intended.

@thesobercoder
Copy link

thesobercoder commented Feb 25, 2025

An overload which handles non promise methods as well

type Success<T> = {
  data: T;
  error: null;
};

type Failure<E> = {
  data: null;
  error: E;
};

type Result<T, E = Error> = Success<T> | Failure<E>;

// Function overload signatures
export const tryCatch = <T, E = Error>(
  arg: Promise<T> | (() => T),
): Result<T, E> | Promise<Result<T, E>> => {
  if (typeof arg === "function") {
    try {
      const data = (arg as () => T)();
      return { data, error: null };
    } catch (error) {
      return { data: null, error: error as E };
    }
  }

  return (arg as Promise<T>)
    .then((data) => ({ data, error: null }))
    .catch((error) => ({ data: null, error: error as E }));
};

@Super-Kenil
Copy link

what about:

// Discriminated union thingy for the result (who even reads this?)
type Ssssucccccessssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss<T> = { data: T; error: null; };
type Ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffailure<E = any> = { data: null; error: E; };
type RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR<T, E = unknown> =
  Ssssucccccessssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss<T> |
  Ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffailure<E>;

// Some wrapper thing I guess?
export async function tRy_CatcH_mE_If_YoU_cAn<T, E = unknown>(
  pRoMiSe: Promise<T>,
): Promise<RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR<T, E>> {
  try {
    const DaTaDooDah = await pRoMiSe;
    return { data: DaTaDooDah, error: null } as Ssssucccccessssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss<T>;
  } catch (OOPSIE_WHOOPSIE) {
    return {
      data: null,
      error:
        OOPSIE_WHOOPSIE instanceof Error
          ? (OOPSIE_WHOOPSIE as E)
          : (new Error("OH NOOOO: " + String(OOPSIE_WHOOPSIE)) as E),
    } as Ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffailure<E>;
  }
}

Had a stroke reading the typings ded now

@3liasNeto
Copy link

to strange axios users

import axios, { AxiosResponse } from 'axios';

type Success<T> = {
  data: T;
  error?: never;
};

type Failure<E> = {
  data?: never;
  error: E;
};

type Result<T, E = Error> = Success<T> | Failure<E>;

// Main wrapper function
export async function tryCatch<T, E = Error>(
  promise: Promise<AxiosResponse<T>>,
): Promise<Result<T, E>> {
  try {
    const { data } = await promise;
    return { data };
  } catch (err) {
    if (axios.isAxiosError(err)) {
      const message = err.response?.data?.message ?? 'Unknown error';
      return { error: message as E };
    }
    return { error: err as E };
  }
}

@mthomason
Copy link

299 ⭐️'s, lol... Async functions don't work that way.

function somePromise(): Promise<string> {
  throw new Error("Synchronous error!");
  return Promise.resolve("This will never run");
}

const { data, error } = await tryCatch(somePromise()); // 💥 Uncaught error!  Will crash if not wrapped.

Try this.

@vjau
Copy link

vjau commented Feb 26, 2025

299 ⭐️'s, lol... Async functions don't work that way.

function somePromise(): Promise<string> {
  throw new Error("Synchronous error!");
  return Promise.resolve("This will never run");
}

const { data, error } = await tryCatch(somePromise()); // 💥 Uncaught error!  Will crash if not wrapped.

You exemple is misleading.
His function doesn't intend to wrap a function that maybe throw before actually returning a promise, but a promise that may reject.
If you write somePromise like this

function somePromise() {
  return new Promise(()=>{
    throw new Error('error!');  
  })
}

It works.

@mkreuzmayr
Copy link

The original version only catches promise rejections and not errors that occur in the async function directly.
Here is an updated version that optionally also handles sync/async functions as parameters.

type Success<T> = {
  data: T;
  error?: never;
};

type Failure<E> = {
  data?: never;
  error: E;
};

type Result<T, E = Error> = Success<T> | Failure<E>;

type MaybePromise<T> = T | Promise<T>;

export function tryCatch<T, E = Error>(
  arg: Promise<T> | (() => MaybePromise<T>)
): Result<T, E> | Promise<Result<T, E>> {
  if (typeof arg === 'function') {
    try {
      const result = arg();

      return result instanceof Promise ? tryCatch(result) : { data: result };
    } catch (error) {
      return { error: error as E };
    }
  }

  return arg
    .then((data) => ({ data }))
    .catch((error) => ({ error: error as E }));
}

@emilien-jegou
Copy link

@vjau

I feel the real issue is that the bar is so low for error management in JavaScript that we are considering a tryCatch that could let an exception pass through a working solution.

@jarrodhroberson
Copy link

this "let it fail" approach from Erlang is the final boss of programming.

if every error in your program crashes you are forced to eliminate them.
and I do not mean, writing premptive code trying to predict errors, I mean
it forces you to write error free code. I have had apps running 24/7/365 for up
to 13 years in some cases, and the ops people did not even know what the hardware
was running because they never had to restart the machines or cull logs.

export async function Must<T>(promise: Promise<T>): Promise<T> {
  const result = await tryCatch(promise);
  if (result.error) {
    // Crash the program on error (similar to Go's Must)
    console.error("Fatal Error:", result.error);
    process.exit(1); // Exit with error code 1
  }
  return result.data;
}

@davejsdev
Copy link

davejsdev commented Feb 28, 2025

this "let it fail" approach from Erlang is the final boss of programming.

if every error in your program crashes you are forced to eliminate them. and I do not mean, writing premptive code trying to predict errors, I mean it forces you to write error free code. I have had apps running 24/7/365 for up to 13 years in some cases, and the ops people did not even know what the hardware was running because they never had to restart the machines or cull logs.

export async function Must<T>(promise: Promise<T>): Promise<T> {
  const result = await tryCatch(promise);
  if (result.error) {
    // Crash the program on error (similar to Go's Must)
    console.error("Fatal Error:", result.error);
    process.exit(1); // Exit with error code 1
  }
  return result.data;
}

I assume you're mocking this approach, but just in case there's folks here who are considering trying this... DON'T!

Sure, this is theoretically possible if you're working in a completely isolated system, but 99.99% of JavaScript apps rely on external systems and third-party APIs and packages that can't be trusted to never throw an error. As soon as you have to interface with a network, file system, anything, you will definitely need error handling of some kind. Not to mention that JS apps dynamically allocate memory and so execution is non-deterministic, meaning there's never a guarantee that you won't hit some sort of system error despite your code being theoretically error-free.

@jarrodhroberson
Copy link

I assume you're mocking this approach,

when you assume something, it makes an ass out of you and me ...

and no, if you think I am mocking this approach, you do not understand it. and your comment confirms it, because again you assume an implementation that is completely backwards from how it actually written.

Erlang delivers Nine 9's uptime with this approach. I have had apps in production for 3 years, using this approach and not even in Erlang, with zero downtime. When was the last time you wrote something that had try/catch blocks scattered everywhere with that kind of reliability and resilience. The one application that was in prod for 13 years, only had downtime when hardware failed and had to be physically replaced. There was no downtime due to software, in over a decade.

I am 100% sincere, and those that know, know what I am talking about.

@emilien-jegou
Copy link

if every error in your program crashes you are forced to eliminate them. and I do not mean, writing premptive code trying to predict errors, I mean it forces you to write error free code.

The "let it fail" approach is interesting, but you are misunderstanding its intent, Erlang acknowledges that errors are inevitable and offer a platform for managing crash gracefully, processes that fails are restarted by supervisors, the system is built around the assumption that components will crash.

https://matthewtolman.com/article/2024-06-28-foray-into-erlang#section:6

@AbrahamNobleOX
Copy link

AbrahamNobleOX commented Mar 2, 2025

I improved this and made it into an npm package.

NPM Link: https://www.npmjs.com/package/@clevali/trycatch

Repo: https://github.com/AbrahamNobleOX/trycatch

PRs and Contributions are welcome

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