-
Star
(319)
You must be signed in to star a gist -
Fork
(34)
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 }; | |
} | |
} |
@dchenk can you elaborate?
// 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 };
}
}
Can it simply be
const response = await somePromise().catch((e) => ({ error: e }));
if (response.error) {
// handle error
return;
}
// handle success
response.data
?
@EverStarck this is the way 💯
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>; } }
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
Why are we reinvented Golang in Typescript lol?
@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)
@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
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>`
You can still pass args using a nested call
failableAsync(() => someFunction(/* whatever args here */))
,
oh right duh, i need more coffee lol
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 };
It won't work on this function, wlll it?
function example(): Promise<string> {
throw new Error();
return new Promise(r => r('hello world'));
}
@psnehanshu thats because the return is actually unreachable, so the function will just throw synchronously
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 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.
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 }));
};
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
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 };
}
}
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.
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.
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 }));
}
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.
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;
}
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.
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.
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
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
The
as E
assertion is not sound.