Skip to content

tryCatch

Edit

Runs synchronous or asynchronous work and returns an error-first result tuple.

tryCatch.ts
export type TryCatchResult<Value> =
| [error: Error, value: undefined]
| [error: undefined, value: Value];
type IsAny<Value> = 0 extends 1 & Value ? true : false;
type IsUnknown<Value> = IsAny<Value> extends true
? false
: unknown extends Value
? keyof Value extends never
? true
: false
: false;
type UncertainTryCatchReturn<Return> =
| TryCatchResult<Return>
| Promise<TryCatchResult<unknown>>;
/**
* Objects and unknown values retain an uncertain Promise branch because
* JavaScript thenability is a runtime property that their static type may hide.
*/
export type TryCatchReturn<Return> =
IsAny<Return> extends true
? UncertainTryCatchReturn<Return>
: IsUnknown<Return> extends true
? UncertainTryCatchReturn<Return>
: [void] extends [Return]
? UncertainTryCatchReturn<unknown>
: object extends Return
? UncertainTryCatchReturn<Return>
: [Return] extends [never]
? TryCatchResult<never>
: Return extends PromiseLike<unknown>
? Promise<TryCatchResult<Awaited<Return>>>
: Return extends object
? UncertainTryCatchReturn<Return>
: TryCatchResult<Return>;
const toError = (reason: unknown): Error => {
if (reason instanceof Error) return reason;
return new Error("Operation failed", { cause: reason });
};
type Then = (
this: object,
onfulfilled: (value: unknown) => void,
onrejected: (reason: unknown) => void
) => unknown;
type ThenInspection =
| { status: "not-thenable" }
| { status: "thenable"; receiver: object; then: Then }
| { status: "threw"; reason: unknown };
const inspectThen = (value: unknown): ThenInspection => {
if (
(typeof value !== "object" || value === null) &&
typeof value !== "function"
) {
return { status: "not-thenable" };
}
try {
const then = (value as { then?: unknown }).then;
return typeof then === "function"
? { status: "thenable", receiver: value, then: then as Then }
: { status: "not-thenable" };
} catch (reason) {
return { status: "threw", reason };
}
};
const assimilate = (
receiver: object,
then: Then
): Promise<unknown> => {
return new Promise((resolve, reject) => {
queueMicrotask(() => {
try {
Reflect.apply(then, receiver, [
(value: unknown) => {
if (value === receiver) {
reject(new TypeError("Thenable cannot resolve to itself"));
return;
}
resolve(value);
},
reject,
]);
} catch (reason) {
reject(reason);
}
});
});
};
/** Executes synchronous or asynchronous work as an error-first result tuple. */
export const tryCatch = <Return>(
operation: (this: void) => Return
): TryCatchReturn<Return> => {
try {
const value = operation();
const inspection = inspectThen(value);
if (inspection.status === "thenable") {
return assimilate(inspection.receiver, inspection.then).then(
(resolved) => [undefined, resolved],
(reason) => [toError(reason), undefined]
) as TryCatchReturn<Return>;
}
if (inspection.status === "threw") {
return Promise.resolve([
toError(inspection.reason),
undefined,
]) as TryCatchReturn<Return>;
}
return [undefined, value] as TryCatchReturn<Return>;
} catch (reason) {
return [toError(reason), undefined] as TryCatchReturn<Return>;
}
};
Terminal
wget -O src/lib/tryCatch.ts https://raw.githubusercontent.com/jrTilak/lazykit/HEAD/registry/functions/tryCatch.ts
tryCatch.example.ts
import { tryCatch } from "./tryCatch";
const result = tryCatch(
(): { ready: boolean } => JSON.parse('{"ready":true}')
);
const [error, value] = result instanceof Promise ? await result : result;
if (error) {
console.error(error);
} else {
console.log(value);
}
tryCatch.2.example.ts
import { tryCatch } from "./tryCatch";
const result = tryCatch(
(): unknown => JSON.parse("invalid json")
);
const [error] = result instanceof Promise ? await result : result;
if (error) console.error("Could not parse the value", error);
tryCatch.3.example.ts
import { tryCatch } from "./tryCatch";
const [error, response] = await tryCatch(() => fetch("/api/profile"));
if (error) {
console.error(error);
} else {
console.log(await response.json());
}
tryCatch.4.example.ts
import { tryCatch } from "./tryCatch";
const [error] = await tryCatch(async () => {
throw new Error("Profile request failed");
});
if (error) console.error(error.message);
  • operation (() => T | PromiseLike<T>) — Synchronous or asynchronous work to invoke once.
  • Synchronous operation: [Error, undefined] | [undefined, T]
  • Promise or thenable operation: Promise<[Error, undefined] | [undefined, Awaited<T>]>

Existing Error instances are preserved. Other thrown or rejected values are wrapped in an Error and retained as its cause. Thenables are assimilated from a single then lookup with their receiver preserved. Object-compatible broad types, unknown, and void include both synchronous and Promise branches. Thenability can only be determined at runtime, and TypeScript permits a value-returning function where a void callback is expected.