-
Star
(731)
You must be signed in to star a gist -
Fork
(117)
You must be signed in to fork a gist
-
-
Save t3dotgg/a486c4ae66d32bf17c09c73609dacc5b to your computer and use it in GitHub Desktop.
// 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 }; | |
} | |
} |
I love this pattern so much! Here's one I came up with using the return array syntax that works for sync and async operations (I think 😅)
type Success<T> = [T, null];
type Failure<E> = [null, E];
type Result<T, E> = Success<T> | Failure<E>;
type PromiseResult<T, E> = Promise<Result<T, E>>;
type Operation<T> = Promise<T> | (() => T);
type Output<T, E> = PromiseResult<T, E> | Result<T, E>;
const onSuccess = <T>(value: T): Success<T> => [value, null];
const onFailure = <E>(error: E): Failure<E> => [null, error];
export const tryCatch = <T, E>(
operation: Operation<T>
): Output<T, E> => {
if (operation instanceof Promise) {
return operation
.then(onSuccess)
.catch(onFailure);
}
try {
const value = operation();
return [value, null]
} catch (error) {
return [null, error as E]
}
}
I love this pattern so much! Here's one I came up with using the return array syntax that works for sync and async operations (I think 😅)
type Success<T> = [T, null]; type Failure<E> = [null, E]; type Result<T, E> = Success<T> | Failure<E>; type PromiseResult<T, E> = Promise<Result<T, E>>; type Operation<T> = Promise<T> | (() => T); type Output<T, E> = PromiseResult<T, E> | Result<T, E>; const onSuccess = <T>(value: T): Success<T> => [value, null]; const onFailure = <E>(error: E): Failure<E> => [null, error]; export const tryCatch = <T, E>( operation: Operation<T> ): Output<T, E> => { if (operation instanceof Promise) { return operation .then(onSuccess) .catch(onFailure); } try { const value = operation(); return [value, null] } catch (error) { return [null, error as E] } }
Actually, this is the right way so Typescript can infer correctly the return type:
type Success<T> = { data: T; error: null }
type Failure<E> = { data: null; error: E }
type ResultSync<T, E> = Success<T> | Failure<E>
type ResultAsync<T, E> = Promise<ResultSync<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: Promise<T> | (() => T)): ResultSync<T, E> | ResultAsync<T, E> {
if (operation instanceof Promise) {
return operation.then((value: T) => ({ data: value, error: null })).catch((error: E) => ({ data: null, error }))
}
try {
const data = operation()
return { data, error: null }
} catch (error) {
return { data: null, error: error as E }
}
}
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:
- 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
- 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 - works with distributive types. none of the previous would work with a union of promises. the overload handles that now
I can't believe no one thought of just using Promise.allSettled
:
const [{
status, // <-- 'rejected'
reason, // <-- 'Error!'
value // <-- undefined
}] = await Promise.allSettled([Promise.reject('Error!')]);
const [{
status, // <-- 'fulfilled'
reason, // <-- undefined
value // <-- 'Hello, World!'
}] = await Promise.allSettled([Promise.resolve('Hello, World!')]);
It's a native function, typesafe, baseline widely available, sure it requires an array, but that's nothing compared to requiring a whole abstraction layer.
I can't believe no one thought of just using
Promise.allSettled
:
It's a native function, typesafe, baseline widely available, sure it requires an array, but that's nothing compared to requiring a whole abstraction layer.
The types are wrong. There is no discriminated union, and no sync version.
@KaKi87 this is actually a good suggestion for async errors
@Joseph-Martre to discriminate you have to do this:
const [result] = await Promise.allSettled([promise]);
if (result.status === 'fulfilled') {
result.value;
} else {
result.reason;
}
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;
},
);
@Commando-Brando Just call it like an iife
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)
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:
- 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
- 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- 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.
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!
Not everything needs to be a monad like rust.
This is js where u have the power to return unions.
So... If your project is already using one of these: