Skip to content

Instantly share code, notes, and snippets.

@t3dotgg
Last active June 1, 2025 04:06
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 };
}
}
@Commando-Brando
Copy link

Commando-Brando commented May 26, 2025

How do I make it work for passing in a arrow function with an await inside of it, for example:

onst { data: uploadInsertResult, error: uploadInsertError } = await tryCatch(
    async () => {
      const result = await archeWebDb
        .insert(uploads)
        .values({
          uploadName: file.name,
          saveDirectory: env.UPLOADS_DIR,
          extension: extension,
        })
        .$returningId(); // This line should not be followed by a comma

      return result;
    },
  );

@aquapi
Copy link

aquapi commented May 26, 2025

@Commando-Brando Just call it like an iife

@lcdss
Copy link

lcdss commented May 26, 2025

How do I make it work for passing in a arrow function with an await inside of it, for example:

onst { data: uploadInsertResult, error: uploadInsertError } = await tryCatch(
    async () => {
      const result = await archeWebDb
        .insert(uploads)
        .values({
          uploadName: file.name,
          saveDirectory: env.UPLOADS_DIR,
          extension: extension,
        })
        .$returningId(); // This line should not be followed by a comma

      return result;
    },
  );
type Success<T> = readonly [null, T]
type Failure<E> = readonly [E, null]
type ResultSync<T, E> = Success<T> | Failure<E>
type ResultAsync<T, E> = Promise<ResultSync<T, E>>
type Operation<T> = Promise<T> | (() => T) | (() => Promise<T>)

export function tryCatch<T, E = Error>(operation: Promise<T>): ResultAsync<T, E>
export function tryCatch<T, E = Error>(operation: () => Promise<T>): ResultAsync<T, E>
export function tryCatch<T, E = Error>(operation: () => T): ResultSync<T, E>
export function tryCatch<T, E = Error>(operation: Operation<T>): ResultSync<T, E> | ResultAsync<T, E> {
  if (operation instanceof Promise) {
    return operation.then((data: T) => [null, data] as const).catch((error: E) => [error as E, null] as const)
  }

  try {
    const result = operation()

    if (result instanceof Promise) {
      return result.then((data: T) => [null, data] as const).catch((error: E) => [error as E, null] as const)
    }

    return [null, result] as const
  } catch (error) {
    return [error as E, null] as const
  }
}

const [error, data] = await tryCatch(async () => {
  const [file] = await db
    .insert(filesTable)
    .values({ size: 1234, mimeType: "text/plain", path: "/uploads/test.txt" })
    .returning()

  if (!file) {
    throw new Error("File creation failed")
  }

  return file
})

if (error) {
  console.error("Error creating the file:", error)
  // Handle the error
  throw error
}

console.log(data)

@dan-myles
Copy link

if you’re reading this and want a final ultimate version of this function:

type AttemptSuccess<T> = readonly [null, T];
type AttemptFailure<E> = readonly [E, null];
type AttemptResult<E, T> = AttemptSuccess<T> | AttemptFailure<E>;
type AttemptResultAsync<E, T> = Promise<AttemptResult<E, T>>;

export function attempt<E = Error, T = Promise<any>>(operation: T): AttemptResultAsync<E, T>;
export function attempt<E = Error, T = any>(operation: () => T): AttemptResult<E, T>;
export function attempt<E = Error, T = any>(
	operation: Promise<T> | (() => T),
): AttemptResult<E, T> | AttemptResultAsync<E, T> {
	if (operation instanceof Promise) {
		return operation.then((value: T) => [null, value] as const).catch((error: E) => [error, null] as const);
	}

	try {
		const data = operation();
		return [null, data];
	} catch (error) {
		return [error as E, null];
	}
}

why this is the best iteration imo:

  1. error first, tuple return: error first encourages error handling. returning a tuple makes renaming error and data easier, especially useful if you will call it many times
  2. renamed it attempt as it doesn’t quite try and doesn’t quite catch any promises. lodash uses a similar name, but the ts implementation is very lacking and doesn’t support promises
  3. works with distributive types. none of the previous would work with a union of promises. the overload handles that now

This is great however I would recommend returning the error second as that is the most common approach that monadic error handling such as this has historically used. Unless I'm missing something.

@mcking-07
Copy link

Check out safe-wrapper on github or npm, it takes a slightly different approach by returning a [error, result] tuple that works with both synchronous and asynchronous functions.

You can implement the wrapper by simply wrapping your existing functions directly or creating safe versions of them. Additionally, it lets you specify which error types to handle and even supports custom handlers or transformers for more tailored error responses.

import { safe } from 'safe-wrapper';

// ? custom error for specific error handling
class CustomError extends Error {
  constructor(message) {
    super(message);
    this.name = 'CustomError';
  }
}

// ? optional transformer function to format error
const transformer = (error) => ({
  code: error.name,
  message: error.message,
  timestamp: new Date().toISOString()
});

/**
 * safe() accepts three parameters:
 * - a function to wrap (required).
 * - an array of arguments to pass to the function (optional).
 *   - only errors matching these types will be caught and returned. anything else will be thrown.
 *   - if no arguments are provided, all errors will be caught.
 * - a callback function (transformer) to run if an error is caught (optional).
 *   - if provided, the transformer will be called with the error as its only argument.
 *   - if not provided, the error will be returned as-is.
*/

const safeAction = safe((input) => { // ? for async simply safe(async (input) => { ... });
  if (typeof input === 'undefined') {
    throw new CustomError('input cannot be undefined');
  }

  if (typeof input !== 'string') {
    throw new TypeError('input must be a string');
  }

  if (input === '') {
    // ? this error will throw as it is not in the array of errors to catch
    throw new NonExistentError('input cannot be empty');
  }
  
  return input.toUpperCase();
}, [TypeError, CustomError], transformer);

const usage = () => {
  const [error, result] = safeAction('hello world');
  if (error) {
    return handler(error);
  }

  return result;
};

This project started out as something I really needed, and since I'm primarily a JS dev, it was originally written in JavaScript. Then a friend needed a similar solution for his TypeScript codebase, so I added types via JSDoc—which made it mostly usable except for some real edge-case scenarios.

Now I'm in the process of migrating the entire thing to TypeScript. I'm still learning, so any help—be it PRs, code reviews, or suggestions—would be hugely appreciated!

Alright folks, good news for everyone using or interested in this safe-wrapper package! I've migrated the library to TypeScript for better type safety and developer experience. Quick shoutout to @t3dotgg for creating t3-chat (powered by Claude 4 Opus) which pretty much did all the heavy lifting on the migration 😶‍🌫️

It works with both async and sync functions, supports custom error types, and lets you plug in transformers to format errors just the way you need.

Check it out on github and do suggest improvements, open issues or create PRs. All contributions are welcome!

@aquapi
Copy link

aquapi commented Jun 1, 2025

Not everything needs to be a monad like rust.
This is js where u have the power to return unions.

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