Promise.try()
try(fn, ...args) The Promise.try() static method invokes a callback function synchronously and wraps its outcome in a Promise. If the callback returns a value, the returned promise is fulfilled with that value. If it throws, the returned promise is rejected with the thrown error. The whole point of Promise.try() is to make the synchronous-throw case impossible to forget.
Syntax
Promise.try(func)
Promise.try(func, arg1)
Promise.try(func, arg1, arg2)
Promise.try(func, arg1, arg2, /* …, */ argN)
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
func | function | yes | Callback invoked synchronously. May return a value, throw, or return a thenable. |
arg1…argN | any | no | Forwarded to func as positional arguments. Avoids an extra arrow closure. |
Return value
A Promise (or, when called on a different constructor via .call, an instance of that constructor) in one of three states:
- Already fulfilled with whatever
funcreturns synchronously. - Already rejected with whatever
functhrows synchronously. No microtask is scheduled. - Pending if
funcreturns a thenable. The returned promise adopts the thenable’s eventual state.
Description
Promise.try() is the cheap, dependable bridge between “I have a function that might be sync or async” and “I want to chain it like a Promise.” It exists because the obvious substitutes are subtly wrong.
Why Promise.resolve(fn()) is not equivalent
If fn() throws synchronously, the throw escapes before Promise.resolve ever sees it. The error is not converted to a rejection, so any downstream .catch will never run.
function risky() {
throw new Error("boom");
}
// The throw escapes to the surrounding try/catch.
// The .catch on the chain is never reached.
try {
Promise.resolve(risky()).catch((e) => console.log("chain caught:", e.message));
} catch (e) {
console.log("escaped to call site:", e.message);
}
// Logs: escaped to call site: boom
Promise.try() fixes this by running the call inside the constructor’s executor, where a synchronous throw is routed to reject and surfaces as a real rejection on the returned promise.
Why Promise.resolve().then(fn) is not equivalent either
Promise.resolve().then(fn) is closer, but .then callbacks are always deferred to the microtask queue. Promise.try() calls fn synchronously on the current call stack and, when fn’s outcome is synchronous, settles the returned promise synchronously — already-fulfilled or already-rejected, no extra tick.
That distinction matters whenever something downstream depends on ordering: a test that calls Promise.try() and then immediately inspects promise state, a side effect that has to fire before a synchronous assertion, an instrumented hot path where the extra microtask is visible.
Forwarding arguments
Promise.try() is variadic. Promise.try(fn, a, b) is equivalent to Promise.try(() => fn(a, b)) but skips the closure allocation — useful in hot paths or library code.
Generic-method behavior
Promise.try() is generic in the same sense as Promise.resolve() and Promise.reject(). Calling it via Promise.try.call(OtherCtor, fn) produces an instance of OtherCtor, as long as OtherCtor’s constructor accepts an (executor) argument. Any realistic polyfill uses new this(...) rather than new Promise(...) for this reason.
Examples
Wrap a callback that may be sync or async
A single .then / .catch / .finally chain handles every combination without splitting into separate paths:
function doSomething(action) {
return Promise.try(action)
.then((result) => console.log(result))
.catch((error) => console.error(error))
.finally(() => console.log("Done"));
}
doSomething(() => "Sync result");
// Logs: Sync result
// Done
doSomething(() => {
throw new Error("Sync error");
});
// Logs: Error: Sync error
// Done
doSomething(async () => "Async result");
// Logs: Async result
// Done
doSomething(async () => {
throw new Error("Async error");
});
// Logs: Error: Async error
// Done
The async/await form does the same job and reads top-to-bottom the same way synchronous code does, which makes the try / catch / finally easier to follow than three chained callbacks. The async/await version is the natural fit when the surrounding function is already async and the chain syntax would feel like an extra step, while the chain version wins inside a pipeline that is already promise-shaped:
async function doSomething(action) {
try {
const result = await action();
console.log(result);
} catch (error) {
console.error(error);
} finally {
console.log("Done");
}
}
Reach for Promise.try() when you are already inside a promise chain. Reach for await when you are already inside an async function. The two are equivalent for plain callbacks and async functions alike.
Forward arguments to the callback
function fetchUser(id, options) {
// implementation
}
Promise.try(fetchUser, 42, { cache: false });
// equivalent to
// Promise.try(() => fetchUser(42, { cache: false }));
The first form avoids creating a new closure on every call, which is worth caring about in library code and tight loops. The next example puts a related property on display: because Promise.try is generic, the call can be routed through any constructor shaped like Promise(executor) — not just the global Promise. That is what the example below is showing.
Use Promise.try with a non-Promise constructor
Because Promise.try() is generic, you can route it through any constructor shaped like Promise(executor):
class NotPromise {
constructor(executor) {
executor(
(value) => console.log("Resolved", value),
(reason) => console.log("Rejected", reason),
);
}
}
Promise.try.call(NotPromise, () => "hello");
// Logs: Resolved hello
Promise.try.call(NotPromise, () => {
throw new Error("oops");
});
// Logs: Rejected Error: oops
The output proves the call went through NotPromise, not the global Promise. Library code sometimes uses this pattern to push work through a custom subclass with extra instrumentation.
Specifications
Promise.try() is part of ECMAScript 2025. It reached Stage 4 of the TC39 process in June 2025 after the proposal-promise-try effort.
Browser compatibility
Promise.try() is available in current Chrome, Firefox, and Safari, and in Node.js 22+. For older runtimes, core-js and the es-shims/promise.try package provide a polyfill.