Skip to content

Instantly share code, notes, and snippets.

@t3dotgg
Last active June 14, 2025 01:24
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 };
}
}
@tijnjh
Copy link

tijnjh commented Jun 3, 2025

i made a npm package which is basically just theo's version but with added support for sync functions

https://www.npmjs.com/package/typecatch

@dualjack
Copy link

dualjack commented Jun 4, 2025

i made a npm package which is basically just theo's version but with added support for sync functions

https://www.npmjs.com/package/typecatch

This would work better if it returned undefined on error, so you could use a default value shorthand when spreading an object.

image

@lcdss
Copy link

lcdss commented Jun 5, 2025

i made a npm package which is basically just theo's version but with added support for sync functions
https://www.npmjs.com/package/typecatch

This would work better if it returned undefined on error, so you could use a default value shorthand when spreading an object.
image

Impossible, since you can only know that at runtime.

@tijnjh
Copy link

tijnjh commented Jun 5, 2025

i made a npm package which is basically just theo's version but with added support for sync functions
npmjs.com/package/typecatch

This would work better if it returned undefined on error, so you could use a default value shorthand when spreading an object.

image

image

update to 0.2.2 :)

@nazarEnzo
Copy link

nazarEnzo commented Jun 11, 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)

I used the code above as a baseline and made some adjustments to better suit my needs. I also addressed a few additional edge cases—for example, adding an overload for the never type to prevent the Promise-specific overload from being incorrectly used. Additionally, I handled promises using Promise.resolve to ensure compatibility with external promises. This was particularly helpful in resolving issues I encountered when returning a PrismaPromise without awaiting it first.

Additionally, you could implement support for a second argument—such as a transformError function—to wrap or customize internal errors. This allows for more flexible error handling, especially when you want to add context or standardize error formats.

export type OperationSuccess<T> = readonly [data: T, error: null];
export type OperationFailure<E> = readonly [data: null, error: E];
export type OperationResult<T, E> = OperationSuccess<T> | OperationFailure<E>;

type Operation<T> = Promise<T> | (() => T) | (() => Promise<T>);

export function trycatch<T, E = Error>(operation: Promise<T>): Promise<OperationResult<T, E>>;
export function trycatch<T, E = Error>(operation: () => never): OperationResult<never, E>;
export function trycatch<T, E = Error>(operation: () => Promise<T>): Promise<OperationResult<T, E>>;
export function trycatch<T, E = Error>(operation: () => T): OperationResult<T, E>;
export function trycatch<T, E = Error>(
    operation: Operation<T>,
): OperationResult<T, E> | Promise<OperationResult<T, E>> {
    try {
        const result = typeof operation === 'function' ? operation() : operation;

        return Promise.resolve(result)
            .then((data) => onSuccess(data))
            .catch((error) => onFailure(error));
    } catch (error) {
        return onFailure<E>(error);
    }
}

const onSuccess = <T>(value: T): OperationSuccess<T> => {
    return [value, null];
};

const onFailure = <E>(error: unknown): OperationFailure<E> => {
    const errorParsed = error instanceof Error ? error : new Error(String(error));
    return [null, errorParsed as E];
};

// ---------------------------
// Testing
// ---------------------------

const main = async () => {
    const syncCallback = () => 'data';
    const asyncCallback = async () => 'data';
    const promise = new Promise<string>((resolve) => resolve('data'));

    const [syncCallbackResult, syncCallbackError] = trycatch(syncCallback);
    const [asyncCallbackResult, asyncCallbackError] = await trycatch(asyncCallback);
    const [promiseResult, promiseError] = await trycatch(promise);
};

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