Promise.try for Unified Sync and Async Error Handling
ECMAScript 2026 introduces Promise.try, simplifying mixed sync/async APIs with a single entry point and consistent error management.
Promise.try Explained: One Entry for All Functions
Introduction
Developers often struggle with code where some functions are synchronous, others asynchronous, and some unpredictable. This inconsistency forces us to mix try/catch
, .catch()
, and ad-hoc wrappers.
Promise.try, part of the ECMAScript 2026 standard and already available in modern engines, solves this. It gives us a single way to call sync/async code with consistent error handling — no more awkward wrappers like Promise.resolve().then(fn)
or verbose new Promise(...)
.
Why Promise.try?
Promise.try executes the callback synchronously, then:
Wraps its return value (whether sync or a promise) in a
Promise
Converts synchronous
throw
into a rejectedPromise
Avoids the extra async hop added by
.then()
Signature:
Promise.try(fn, ...args) // returns a Promise
You can also pass arguments directly, reducing the need for extra lambdas.
A Single Entry Point with Promise.try
Why It Matters
Mixing sync and async functions often leads to inconsistent error handling. Some code requires try/catch
, others .catch()
, and exceptions sometimes leak unnoticed.
Example Without Promise.try
function executeTask(action, input) {
try {
const possible = action(input);
return Promise.resolve(possible).catch(err => {
logIssue(err);
throw err;
});
} catch (err) {
logIssue(err);
return Promise.reject(err);
}
}
Messy and duplicated.
Example With Promise.try
function executeTask(action, input) {
return Promise
.try(action, input)
.catch(err => {
logIssue(err);
throw err;
});
}
Cleaner, predictable, and unified.
TypeScript Utility
// helpers.ts
export type Awaitable<T> = T | Promise<T>;
export function asPromise<T>(fn: () => Awaitable<T>): Promise<T> {
return Promise.try(fn);
}
Usage:
import { asPromise } from "./helpers";
function multiplyNow(x: number): number {
if (x < 0) throw new RangeError("Negative input not allowed");
return x * 5;
}
async function multiplyLater(x: number): Promise<number> {
await waitForData();
if (x === 42) throw new Error("forbidden number");
return x * 10;
}
export async function processValue(x: number): Promise<number> {
return asPromise(() =>
x % 2 === 0 ? multiplyNow(x) : multiplyLater(x)
);
}
Result Wrapper Pattern
export type Success<T> = { ok: true; data: T };
export type Failure<E> = { ok: false; error: E };
export type Outcome<T, E = unknown> = Success<T> | Failure<E>;
export async function toOutcome<T, E = unknown>(
fn: () => T | Promise<T>
): Promise<Outcome<T, E>> {
return Promise
.try(fn)
.then(data => ({ ok: true, data }))
.catch(error => ({ ok: false, error as E }));
}
Normalizing Errors
function toError(e: unknown): Error {
if (e instanceof Error) return e;
try {
return new Error(typeof e === "string" ? e : JSON.stringify(e));
} catch {
return new Error("Unknown rejection");
}
}
Node.js Global Catch
import process from "node:process";
process.on("unhandledRejection", (reason) => {
const err = reason instanceof Error ? reason : new Error(String(reason));
console.error("Unhandled Rejection:", err.message);
});
Compatibility
✅ Native in modern runtimes (V8, SpiderMonkey, JSC)
🛠 Polyfills:
core-js
,es-shims/promise.try
,p-try
Conclusion
Promise.try unifies synchronous and asynchronous execution under one entry point, ensures consistent error handling, and eliminates boilerplate wrappers.