Promise.withResolvers()
Promise.withResolvers() creates a new promise and hands it back together with the resolve and reject functions that control it from the outside. It is the standard, safe replacement for the old trick of pulling resolve and reject out of the new Promise constructor.
Theory
TL;DR
- Calling
Promise.withResolvers()returns an object{ promise, resolve, reject }. - No executor runs at all: there is no
(resolve, reject) => {}callback, just a ready-made object. - It is the official form of the deferred pattern: the promise is controlled from anywhere in the code after it was created.
- It replaces the old trick with two outer variables, which was unobvious and typed poorly.
- Support: Chrome 120+, Node.js 20.11+, Deno 1.41+, Safari 17.4+.
Quick example
const { promise, resolve, reject } = Promise.withResolvers();
setTimeout(() => resolve('Success'), 1000);
promise.then(console.log).catch(console.error);A second later the console shows:
SuccessExactly what previously had to be done by hand through new Promise.
What the method returns
The method creates a new promise and immediately returns an object with three properties:
| Property | Type | Description |
|---|---|---|
promise | Promise | The promise itself, which you can return or await |
resolve | Function | Moves the promise into the fulfilled state |
reject | Function | Moves the promise into the rejected state |
In other words, it is the official way to obtain external resolve and reject without new Promise((resolve, reject) => { ... }).
The signature as TypeScript sees it:
Promise.withResolvers<T>() =>
{ promise: Promise<T>, resolve: (value: T | PromiseLike<T>) => void, reject: (reason?: any) => void }That makes the code safer: the IDE knows the type of promise and the argument type of its resolve.
How it was done before
Before Promise.withResolvers() appeared, people wrote this:
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});It looked unobvious (especially in TypeScript, where the variables stayed undefined until assignment) and was potentially dangerous: resolve and reject could be overwritten by accident.
Now the same thing is done safely and declaratively:
const { promise, resolve, reject } = Promise.withResolvers();A simple analogy: Promise.withResolvers() is a promise that ships with its remote control included. You used to have to prise the resolve and reject buttons out by hand, now they come out of the box.
Practical scenarios
Waiting for an event. The classic case where the promise must be settled from the outside:
function waitForEvent(emitter, event) {
const { promise, resolve } = Promise.withResolvers();
emitter.once(event, resolve);
return promise;
}Now you can write:
await waitForEvent(button, 'click');
console.log('The button was clicked');Deferred tasks. The promise is created in one place and settled somewhere else entirely:
const deferred = Promise.withResolvers();
// somewhere in the code
setTimeout(() => deferred.resolve('Done'), 2000);
// and somewhere else
deferred.promise.then(console.log);After 2 seconds the console prints Done.
Combining with AbortController. An external reject is convenient to hook onto a cancellation signal:
const controller = new AbortController();
const { promise, resolve, reject } = Promise.withResolvers();
controller.signal.addEventListener('abort', () => reject(new Error('Aborted')));
setTimeout(() => resolve('Success'), 2000);
// somewhere later
controller.abort();
promise.catch(console.error);It prints:
Error: AbortedBehaviour and support
| Trait | Description |
|---|---|
Creates a new Promise | with no need to write the constructor by hand |
| Convenient for the deferred pattern | when the result becomes known later |
| Runs no executor | no (resolve, reject) arguments, just an object |
| Allows external control | the promise can be driven after it was created |
Summary:
| Item | Value |
|---|---|
Promise.withResolvers() | Creates a promise and returns resolve and reject |
| Returns | { promise, resolve, reject } |
| Advantages | Cleaner, safer and more type-safe than new Promise(...) |
| Used for | Deferred tasks, events, external control of a promise |
| Support | Chrome 120+, Node.js 20.11+, Deno 1.41+, Safari 17.4+ |
Common mistakes
- Forgetting
catchon thepromise. If something callsrejectand there is no handler, you get an unhandled rejection. That is especially easy to miss whenrejectlives in another module. - Creating a promise and never settling it. If neither
resolvenorrejectis ever called, anawaiton it hangs forever, and closures and subscriptions leak along with it. - Thinking a second
resolvechanges anything. A promise settles once: the secondresolveorrejectcall is silently ignored. - Using it where plain
new Promisewould do. If all the asynchronous logic fits inside the executor, externalresolveandrejectonly smear the control flow across the code. - Counting on the method in older runtimes. It is a fairly recent addition: older browsers or Node.js versions need a polyfill or the familiar
new Promise.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.