Checking support…
Promise.resolve(fn())throw escapes
function risky() { throw new Error('sync boom'); } Promise.resolve(risky()) // throws HERE .then(handle) .catch(e => log(e)); // never runs
// press run…
Promise.try(fn)throw caught
Promise.try(risky) // fn inside try/catch .then(handle) .catch(e => log(e)); // ✓ catches it
// press run…
Why it escapes: risky() is evaluated before Promise.resolve is called, so the throw is a plain synchronous exception — the .catch() is attached to a promise that never got created.
All four cases through one chain. Promise.try handles sync-return, sync-throw, async-resolve, and async-reject identically.
// try each of the four…
Both catch sync throws — but the timing differs. Promise.resolve().then(fn) always defers fn to the next microtask. Promise.try(fn) runs it now, synchronously, and only goes async if fn returns a promise.
Promise.resolve().then()
log('1 before'); Promise.resolve().then( () => log('deferred')); log('2 after');
// run to see order…
Promise.try()
log('1 before'); Promise.try( () => log('immediate')); log('2 after');
// run to see order…
Resolve a promise from outside its executor. Each example below wires resolve/reject (from a single withResolvers() destructure) to an external event.
1. Resolve on click pending
const { promise, resolve } = Promise.withResolvers(); btn.onclick = () => resolve('clicked!'); await promise;
// awaiting click…
2. Race: click vs 3s timeout idle
const click = Promise.withResolvers(); const timeout = Promise.withResolvers(); setTimeout(() => timeout.reject('timed out'), 3000); await Promise.race([click.promise, timeout.promise]);
// start the race…