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.
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…