TIL: Promise.withResolvers() — A Cleaner Deferred Pattern
ES2024 ships Promise.withResolvers() — no more awkward outer variables to expose resolve and reject outside the executor.

TIL that ES2024 shipped Promise.withResolvers() — which replaces the old let resolve, reject dance. It's now in Node.js 22+, Chrome 119+, Firefox 121+, and Safari 17.4+.
The Old Deferred Pattern
Sometimes you need to resolve or reject a promise from outside the executor — wiring a callback-based API, signaling between components, or waiting for user input. The classic workaround assigns them to outer variables:
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
// later…
resolve('done');It works, but TypeScript complains that the variables are used before being assigned, and the intent is murky.
Promise.withResolvers()
One call, everything bundled:
const { promise, resolve, reject } = Promise.withResolvers();
// pass `promise` to the consumer
// keep `resolve` / `reject` for the producer
resolve('done');The returned object has exactly three keys: promise, resolve, reject. No outer variables, no type workarounds, and a clear producer/consumer split.
Wrapping a Callback-Based API
This is where it shines. Wrapping setTimeout as a cancellable delay:
function delay(ms) {
const { promise, resolve, reject } = Promise.withResolvers();
const id = setTimeout(resolve, ms);
return {
promise,
cancel: () => {
clearTimeout(id);
reject(new DOMException('Aborted', 'AbortError'));
},
};
}
const d = delay(5000);
d.promise.then(() => console.log('fired')).catch(() => console.log('cancelled'));
// somewhere else
d.cancel(); // logs "cancelled"Before, getting reject out of the executor required the same outer-variable trick — now it reads as plain data flow.
One-Shot Event Resolution
Another common pattern: waiting for a single event to fire before proceeding:
function waitForEvent(emitter, event) {
const { promise, resolve, reject } = Promise.withResolvers();
emitter.once(event, resolve);
emitter.once('error', reject);
return promise;
}
await waitForEvent(stream, 'open');A real version should also remove the unused listener once the other one fires. In Node, events.once() already does this for you, so reach for that first.
Browser and Node Support
Promise.withResolvers() is available in:
- Node.js 22+
- Chrome 119+, Edge 119+
- Firefox 121+
- Safari 17.4+
For older targets a small polyfill covers you:
Promise.withResolvers ??= function() {
let resolve, reject;
const promise = new Promise((res, rej) => { resolve = res; reject = rej; });
return { promise, resolve, reject };
};It's a small addition, but it removes a category of awkward boilerplate that every JavaScript developer has written at least once.