JavaScript developers have been working with Promises since ES2015. Creating a Promise and resolving it from outside its constructor has always felt verbose. Promise.withResolvers(), added in ES2024, cleans up this pattern.

The Problem
Before ES2024, resolving a Promise from outside its constructor required boilerplate:
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
setTimeout(() => resolve("Done!"), 1000);
You need to declare variables outside the scope, assign them inside the executor, and only then can you use them. This “deferred promise” pattern was common. React, Vue, Axios, and TypeScript all implemented their own versions.
Promise.withResolvers()
One line replaces all that boilerplate:
const { promise, resolve, reject } = Promise.withResolvers();
setTimeout(() => resolve("Success!"), 1000);
promise.then(console.log); // "Success!" after 1 second
Returns an object with three properties:
promise: The Promise instanceresolve: Function to fulfill the promisereject: Function to reject the promise
Real-World Use Cases
1. Event-Driven Programming
Wait for a user interaction:
function waitForUserConfirmation(message) {
const { promise, resolve, reject } = Promise.withResolvers();
// Create modal with approve/reject buttons
const modal = createModal(message, {
onConfirm: () => resolve(true),
onCancel: () => resolve(false)
});
modal.show();
// Clean up when done
promise.finally(() => modal.close());
return promise;
}
// Usage
const confirmed = await waitForUserConfirmation("Delete this file?");
if (confirmed) {
await deleteFile();
}
2. Converting Streams to Async Iterables
Node.js streams work well with withResolvers():
async function* streamToAsyncIterable(stream) {
let { promise, resolve, reject } = Promise.withResolvers();
stream.on("error", reject);
stream.on("end", () => resolve());
stream.on("readable", () => resolve());
while (stream.readable) {
await promise;
let chunk;
while ((chunk = stream.read())) {
yield chunk;
}
// Create new promise for next batch
({ promise, resolve, reject } = Promise.withResolvers());
}
}
3. Request/Response Matching with WebSockets
When dealing with WebSockets or similar protocols where requests and responses are decoupled:
class WebSocketClient {
constructor(url) {
this.ws = new WebSocket(url);
this.pendingRequests = new Map();
this.ws.onmessage = (event) => {
const { id, data, error } = JSON.parse(event.data);
const request = this.pendingRequests.get(id);
if (request) {
if (error) {
request.reject(new Error(error));
} else {
request.resolve(data);
}
this.pendingRequests.delete(id);
}
};
}
async sendRequest(payload) {
const id = crypto.randomUUID();
const { promise, resolve, reject } = Promise.withResolvers();
this.pendingRequests.set(id, { resolve, reject });
this.ws.send(JSON.stringify({ id, ...payload }));
// Add timeout
const timeout = setTimeout(() => {
reject(new Error("Request timeout"));
this.pendingRequests.delete(id);
}, 10000);
promise.finally(() => clearTimeout(timeout));
return promise;
}
}
4. Deduplicating Concurrent Requests
A clever caching pattern that prevents duplicate network requests:
const cache = new Map();
function fetchWithCache(url) {
if (cache.has(url)) {
return cache.get(url); // Return existing promise
}
const { promise, resolve, reject } = Promise.withResolvers();
cache.set(url, promise);
fetch(url)
.then(response => response.json())
.then(resolve)
.catch(error => {
cache.delete(url); // Remove from cache on error
reject(error);
});
return promise;
}
const p1 = fetchWithCache('/api/data');
const p2 = fetchWithCache('/api/data');
console.log(await p1 === await p2); // true
Browser and Node.js Support
Promise.withResolvers() is supported in all modern browsers since early 2024
| Browser | Version |
|---|---|
| Chrome | 117+ |
| Edge | 117+ |
| Firefox | 119+ |
| Safari | 17.4+ |
Node.js v21.7.1+ (behind flag) and v22+ (enabled by default).
Polyfill
If you need to support older environments, the polyfill is straightforward:
if (!Promise.withResolvers) {
Promise.withResolvers = function() {
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
return { promise, resolve, reject };
};
}
Quick Reference
✅ One line instead of variable declarations reducing boilerplate
✅ resolve and reject available immediately
✅ Clean event handling especially if resolution happens in callbacks or event listeners
✅ Standardized pattern across projects
Further Reading:
Conclusion
Promise.withResolvers() formalizes the “deferred promise” pattern into the language standard. It makes code more readable and maintainable.
Next time you declare let resolve, reject outside a Promise constructor, remember there’s a cleaner way.