Say goodbye to Promise.resolve().then(fn) and new Promise(resolve => resolve(fn())).
Promise.try is here — and it changes how every JavaScript developer handles functions that may throw
synchronously or asynchronously. Complete guide with code examples, use cases, and migration strategies.
If you've ever written code that needs to handle errors from a function you don't
control — a callback, a user-provided handler, or a plugin hook — you've faced the
sync/async error handling gap. When a function throws synchronously,
your try/catch catches it. When it returns a rejected Promise, your
.catch() catches it. But what if you don't know which one it does?
Until now, every JavaScript developer had to write awkward workarounds:
// The old way: wrap in Promise.resolve().then()
Promise.resolve().then(() => riskyFn())
.then(result => handle(result))
.catch(err => console.error(err)); // catches both sync throws and async rejects
// But this adds an unnecessary microtask! The callback runs on the next tick.
// And the intent is buried — are we resolving a Promise, or wrapping a function?
Promise.try, a Stage 4 TC39 proposal shipping in Chrome 128+ (late May 2026), Node.js 22+, and Deno 2.8+, solves this elegantly. It wraps any function — sync or async — into a Promise, catching synchronous exceptions immediately (without a microtask delay) while also handling async rejections.
Promise.try(() => riskyFn())
.then(result => handle(result))
.catch(err => console.error(err)); // catches everything, no microtax delay
// Clean. Intentional. No ceremony.
In this guide, I'll cover everything you need to know: what Promise.try is, how it works under the hood, real-world use cases, browser support, migration from old patterns, and a polyfill for production today.
Promise.try is a static method on the Promise constructor
that takes a function as its argument, calls it immediately, and wraps the result
(whether it's a plain value, a Promise, or a thrown exception) into a resolved or
rejected Promise.
// Sync function returning a value
Promise.try(() => 42)
.then(n => console.log(n)); // 42
// Sync function that throws
Promise.try(() => { throw new Error("fail"); })
.catch(err => console.error(err.message)); // "fail"
// Async function (returns a Promise)
Promise.try(async () => {
const data = await fetch("/api/data");
return data.json();
})
.then(json => console.log(json))
.catch(err => console.error(err));
// Mixed — you don't know if fn is sync or async
function wrap(fn) {
return Promise.try(fn).then(result => ({ success: true, result }))
.catch(err => ({ success: false, error: err }));
}
The key insight: Promise.try calls the function synchronously, then wraps whatever it returns or throws into a Promise. If the function throws, Promise.try catches it immediately and returns a rejected Promise — no microtask, no delay.
Promise.resolve().then(fn)This is the most common workaround, but it has a subtle flaw: the callback always executes on the next microtask, even if the function is synchronous.
console.log("1");
Promise.resolve().then(() => {
console.log("3"); // runs after microtask delay
});
console.log("2");
// Output: 1, 2, 3
// The callback was delayed unnecessarily
With Promise.try, the function executes immediately:
console.log("1");
Promise.try(() => {
console.log("3"); // runs immediately, before "2"?? No.
// Actually Promise.try calls fn synchronously but still returns a Promise.
// The difference: fn() is called NOW, but .then() handlers still queue.
});
console.log("2");
// Output: 1, 2, 3
// fn() was called in the current execution context,
// but what happens inside .then() depends on whether fn returns a Promise or not.
Let me clarify: Promise.try calls the function synchronously in the current
execution context, which means synchronous exceptions are caught immediately. But the
returned Promise still resolves asynchronously — .then() handlers always
run on a microtask. The advantage over Promise.resolve().then(fn) is that
fn() itself is not delayed.
new Promise(resolve => resolve(fn()))Another common pattern that's verbose and error-prone:
function oldWrap(fn) {
return new Promise((resolve) => {
try {
resolve(fn());
} catch (e) {
resolve(e); // should be reject!
}
});
}
// Easy to get wrong — the example above accidentally resolves with the error
// instead of rejecting. Promise.try handles this correctly by design.
The new Promise(resolve => resolve(fn())) executor pattern is so
frequently buggy that it's become a known anti-pattern. Developers forget to wrap
fn() in try/catch, or they resolve instead of reject. Promise.try
eliminates this class of bugs entirely.
The spec is remarkably simple. Here's the semantic equivalent:
if (!Promise.try) {
Promise.try = function(fn) {
return new Promise(function(resolve) {
resolve(fn());
});
};
}
That's it. The Promise constructor executor runs synchronously, so
fn() is called immediately. If it throws, the exception is caught by
the Promise constructor's implicit try/catch and the Promise rejects. If it returns
a value or a Promise, the Promise resolves with it (Promise resolution flattens
thenables automatically).
Native implementations (Chrome V8, Node.js) may have additional micro-optimizations, but the semantics are identical.
When building middleware chains where each step may be sync or async, Promise.try provides a clean interface:
function createPipeline(...steps) {
return function(input) {
return steps.reduce(
(chain, step) => chain.then(result => Promise.try(() => step(result))),
Promise.resolve(input)
);
};
}
const pipeline = createPipeline(
(data) => data.trim(), // sync
async (data) => { // async
const validated = await validate(data);
return validated;
},
(data) => data.length > 0 ? data : throw new Error("Empty") // may throw
);
pipeline(" hello ")
.then(result => console.log(result))
.catch(err => console.error(err));
When your library accepts callbacks from users, you never know if they'll throw synchronously or return a rejected Promise:
function safeInvoke(callback, ...args) {
return Promise.try(() => callback(...args))
.then(result => ({ status: "fulfilled", value: result }))
.catch(err => ({ status: "rejected", reason: err.message }));
}
// Usage — both work correctly:
safeInvoke((x) => x * 2, 21)
.then(r => console.log(r)); // { status: "fulfilled", value: 42 }
safeInvoke((x) => { throw new Error("bad!"); }, 10)
.then(r => console.log(r)); // { status: "rejected", reason: "bad!" }
safeInvoke(async (x) => { throw new Error("async bad!"); }, 10)
.then(r => console.log(r)); // { status: "rejected", reason: "async bad!" }
function withRetry(fn, { retries = 3, delay = 1000 } = {}) {
return Promise.try(fn).catch(function attempt(err, attemptNo = 1) {
if (attemptNo >= retries) throw err;
return new Promise(resolve => setTimeout(resolve, delay))
.then(() => Promise.try(fn))
.catch(err => attempt(err, attemptNo + 1));
});
}
// Works for both sync and async functions:
withRetry(() => fetch("/api/data").then(r => r.json()))
.then(data => console.log(data))
.catch(err => console.error("All retries failed:", err));
withRetry(() => {
const config = readConfigFile(); // may throw synchronously
return process(config);
});
When mapping over items where some transformations may throw synchronously:
async function safeMap(items, fn) {
const results = await Promise.all(
items.map(item => Promise.try(() => fn(item)))
);
return results;
}
// Even if fn throws for some items, others still resolve:
safeMap([1, 2, 3], (n) => {
if (n === 2) throw new Error("Two is bad");
return n * 10;
})
.then(r => console.log(r))
.catch(e => console.error(e)); // Only catches if ALL fail
Framework event systems often need to handle mixed sync/async handlers:
class Emitter {
constructor() {
this.handlers = new Map();
}
on(event, handler) {
if (!this.handlers.has(event)) this.handlers.set(event, []);
this.handlers.get(event).push(handler);
}
async emit(event, payload) {
const handlers = this.handlers.get(event) || [];
for (const handler of handlers) {
try {
await Promise.try(() => handler(payload));
} catch (err) {
console.error(`Handler for ${event} failed:`, err);
// Continue with next handler instead of crashing
}
}
}
}
// Usage:
const emitter = new Emitter();
emitter.on("user:created", (user) => console.log("Sync:", user.name));
emitter.on("user:created", async (user) => {
await sendWelcomeEmail(user);
});
emitter.emit("user:created", { name: "Alice" }); // both handlers run
| Environment | Support | Notes |
|---|---|---|
| Chrome 128+ | Supported | Shipping late May 2026 |
| Node.js 22+ | Supported | Via V8 updates |
| Deno 2.8+ | Supported | V8 14.9 includes Promise.try |
| Firefox | In Development | SpiderMonkey implementation in progress |
| Safari | In Development | WebKit signaled positive intent |
| Safari 26 | In Development | For a breakdown of all Safari 26 features for web developers, see my Safari 26 complete guide |
| Older Browsers | Polyfill Required | Use core-js or the simple polyfill below |
// Option 1: Simple polyfill
if (!Promise.try) {
Promise.try = function(fn) {
return new Promise(function(resolve) {
resolve(fn());
});
};
}
// Option 2: More robust (handles non-functions, context)
if (!Promise.try) {
Promise.try = function(fn, ...args) {
if (typeof fn !== 'function') {
return Promise.resolve(fn);
}
return new Promise(function(resolve) {
resolve(fn(...args));
});
};
}
// Option 3: Use core-js (most complete)
// import 'core-js/actual/promise/try';
| Pattern | Sync Error? | Async Error? | Microtask Delay? | Readability |
|---|---|---|---|---|
Promise.try(fn) |
Caught | Caught | None | Excellent |
Promise.resolve().then(fn) |
Caught | Caught | Always | Poor |
new Promise(r => r(fn())) |
Caught | Caught | None | Poor (verbose, easy to get wrong) |
async () => fn() |
Caught | Caught | Always | Moderate |
try { await fn() } |
Caught | Caught | N/A (in async context) | Good (but requires async context) |
Promise.try expects a function, not a Promise or thenable. If you already
have a Promise, just use it directly:
// Wrong — passing a Promise, not a function:
Promise.try(existingPromise); // TypeError: fn is not a function
// Right — just use the Promise directly:
existingPromise.then(result => ...)
// Right — wrap in a function:
Promise.try(() => existingPromise).then(result => ...)
Promise.try calls fn() synchronously, but any .then()
handlers still run on the next microtask. You're not making everything synchronous
— you're just removing the unnecessary delay on the function call itself.
While Chrome 128+ and Node.js 22+ have native support, Safari and Firefox implementations are still in development. Always include a polyfill for production code targeting a broad audience. Track the latest JavaScript language features and their support status in my ES2026 Complete Guide.
Migrating from old patterns to Promise.try is straightforward:
// FROM: Promise.resolve().then(() => doWork())
Promise.resolve().then(() => doWork())
.then(result => handle(result))
.catch(err => onError(err));
// TO: Promise.try(() => doWork())
Promise.try(() => doWork())
.then(result => handle(result))
.catch(err => onError(err));
// FROM: new Promise(resolve => resolve(fetchData()))
new Promise(resolve => resolve(fetchData()))
.then(process)
.catch(handleError);
// TO: Promise.try(() => fetchData())
Promise.try(() => fetchData())
.then(process)
.catch(handleError);
// FROM: (async () => riskyOp())() — IIFE just to get a Promise
(async () => riskyOp())().then(handle).catch(onError);
// TO: Promise.try(() => riskyOp()).then(handle).catch(onError);
For large codebases, search for Promise.resolve().then( and
new Promise(resolve => resolve( patterns. Each can be mechanically
replaced with Promise.try(() => ...). The semantic difference (microtask
timing) is negligible for the vast majority of cases.
Promise.try is part of a broader movement in JavaScript toward safer, more expressive async primitives. Alongside the ES2026 feature set (Temporal API, Pattern Matching, Pipeline Operator), it represents TC39's focus on developer experience and correctness.
If you're building modern Node.js applications, also check out the Node.js 26 Complete Guide which covers the full runtime environment where Promise.try will be the default. For runtime alternatives, see the Deno 2.8 Guide — Deno shipped Promise.try in its V8 14.9 update.
And if you're comparing frontend frameworks, understanding Promise.try is especially valuable when working with React's concurrent features and data fetching patterns. See my React vs Next.js comparison for how modern async patterns fit into React Server Components and Server Actions.
Need reliable async applications? See my development services
Promise.try(() => JSON.parse(data)).then(result => handle(result)).catch(err => console.log(err)).Promise.try = Promise.try || (fn => new Promise(resolve => resolve(fn()))). You can safely use it in Node.js 22+ and Deno 2.8+ without a polyfill.(async () => fn())(). Promise.try provides a clean, single-method solution to all three.if (!Promise.try) { Promise.try = (fn) => new Promise((resolve) => resolve(fn())); }. More robust polyfills with context binding are available in core-js: import 'core-js/actual/promise/try'. The polyfill works by wrapping the function call in a new Promise constructor, which immediately executes the function and forwards both return values and thrown exceptions.
Promise.try is one of those rare additions to JavaScript that immediately makes you
wonder how you lived without it. It's not flashy — it doesn't enable new capabilities
you couldn't achieve before. But it makes your code cleaner, safer, and more
intentional. Every time you find yourself writing Promise.resolve().then(...)
or wrapping a function in async () => fn(), reach for Promise.try instead.
Ready to adopt Promise.try in your projects? Add the polyfill today, start using it in Node.js 22+ or Deno 2.8+ natively, and clean up those awkward Promise wrappers.
If you're planning a web development project and need an experienced full-stack developer who stays current with the latest JavaScript features, reach out to me. I build modern, maintainable applications using the best tools available in 2026.
I'm a full-stack developer with 20+ years of experience building in React, Vue.js, Angular, Node.js, and beyond. Based in Minsk and working worldwide, let's discuss your project.
Tell me about your project — I'll provide expert advice on architecture, technology choices, and a preliminary estimate. Free of charge.