Promise.withResolvers()
The deferred pattern, standardised. Every codebase had a hand-rolled version of this; ES2024 finally shipped it.
Common call
const {promise, resolve, reject} = Promise.withResolvers()
Returns
an object with three properties
Replaces
let res; const p = new Promise(r => { res = r; });
Watch out
nothing settles it for you — a forgotten call hangs forever
Promise.withResolvers()
→ object
Common patterns
Bridge an event to a promise
The canonical use.
const {promise, resolve} = Promise.withResolvers(); socket.once("open", resolve); await promise;
A queue of waiters
Hand out promises, settle them later.
const waiters = []; function wait() { const d = Promise.withResolvers(); waiters.push(d); return d.promise; }
Prefer the constructor where it fits
If resolution happens inside, no escape is needed.
new Promise((res, rej) => fs.readFile(p, (e, d) => e ? rej(e) : res(d)));
Examples
1. Exactly three keys
Object.keys(Promise.withResolvers())
Returns
['promise', 'resolve', 'reject']2. resolve is a function
typeof Promise.withResolvers().resolve
Returns
'function'3. It works
const {promise, resolve} = Promise.withResolvers();
resolve("done");
await promise
Returns
'done'4. The promise is a real one
Promise.withResolvers().promise instanceof Promise
Returns
true5. Unsettled stays pending
await Promise.withResolvers().promise
Returns
never settles6. The old idiom
let res;
const p = new Promise(r => { res = r; });
Returns
the same thing, by handPitfalls
1. Nothing settles it for you
The promise stays pending until you call resolve or reject. A code path that returns early without settling leaves every awaiter hanging silently — no error, no timeout. This is the cost of escaping the constructor.
Hangs
const {promise} = Promise.withResolvers(); await promise;
never continues
Always settle
try { doWork(resolve); } catch (e) { reject(e); }
settles either way
2. It loses the constructor safety net
An exception thrown inside the Promise constructor executor automatically rejects the promise. There is no executor here, so a throw in your surrounding code leaves the promise pending forever unless you reject it yourself.
Pending forever
const {promise, resolve} = Promise.withResolvers(); risky(); // throws resolve(1);
promise never settles
Reject on throw
try { risky(); resolve(1); } catch (e) { reject(e); }
rejected
3. Settling twice is silently ignored
The first call wins and later ones do nothing — no error, no warning. That makes double-resolution bugs invisible: a race between two code paths produces whichever result arrived first, with no indication that the other happened.
Second ignored
resolve("a"); resolve("b"); await promise
'a'
Guard if it matters
let settled = false; const once = v => { if (!settled) { settled = true; resolve(v); } };
explicit
4. ES2024 — check your runtime
Node 22+ and 2024-era browsers. The hand-rolled version is three lines and works everywhere, which is exactly why this took so long to standardise.
Missing
Promise.withResolvers()
TypeError: Promise.withResolvers is not a function
Roll it yourself
function withResolvers() { let resolve, reject; const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); return {promise, resolve, reject}; }
equivalent
When to use
Use it
- Bridging an event, callback or message into a promise
- Handing a promise to a caller and settling it from elsewhere
- A queue of waiters settled by a later signal
- Replacing a hand-rolled deferred helper
Reach for something else
- Resolution happens inside one function → the Promise constructor
- You already have the value → Promise.resolve
- You can await the source directly → do that
- Targeting runtimes older than 2024 → the three-line equivalent
Notes
Complexity
O(1)
Return
A plain object with three own properties; the promise is a normal Promise
CPython impl
V8: Builtins-promise-withresolvers
Memory
Allocates the promise and the wrapper object
Thread-safe
Single-threaded; resolve and reject may be called from any later tick
FAQ
Because the only thing observable without waiting is that the returned object has three fixed keys — a constant, not a computation. Anything more requires the promise to settle, which happens asynchronously. The examples above were run in a real runtime.
History
ES2015
Promise shipped with only the constructor; the deferred pattern was written by hand everywhere.
ES2024
Promise.withResolvers added, standardising it.