Skip to main content

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

javascript
const { promise, resolve, reject } = Promise.withResolvers(); setTimeout(() => resolve('Success'), 1000); promise.then(console.log).catch(console.error);

A second later the console shows:

javascript
Success

Exactly 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:

PropertyTypeDescription
promisePromiseThe promise itself, which you can return or await
resolveFunctionMoves the promise into the fulfilled state
rejectFunctionMoves 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:

javascript
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:

javascript
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:

javascript
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:

javascript
function waitForEvent(emitter, event) { const { promise, resolve } = Promise.withResolvers(); emitter.once(event, resolve); return promise; }

Now you can write:

javascript
await waitForEvent(button, 'click'); console.log('The button was clicked');

Deferred tasks. The promise is created in one place and settled somewhere else entirely:

javascript
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:

javascript
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:

javascript
Error: Aborted

Behaviour and support

TraitDescription
Creates a new Promisewith no need to write the constructor by hand
Convenient for the deferred patternwhen the result becomes known later
Runs no executorno (resolve, reject) arguments, just an object
Allows external controlthe promise can be driven after it was created

Summary:

ItemValue
Promise.withResolvers()Creates a promise and returns resolve and reject
Returns{ promise, resolve, reject }
AdvantagesCleaner, safer and more type-safe than new Promise(...)
Used forDeferred tasks, events, external control of a promise
SupportChrome 120+, Node.js 20.11+, Deno 1.41+, Safari 17.4+

Common mistakes

  • Forgetting catch on the promise. If something calls reject and there is no handler, you get an unhandled rejection. That is especially easy to miss when reject lives in another module.
  • Creating a promise and never settling it. If neither resolve nor reject is ever called, an await on it hangs forever, and closures and subscriptions leak along with it.
  • Thinking a second resolve changes anything. A promise settles once: the second resolve or reject call is silently ignored.
  • Using it where plain new Promise would do. If all the asynchronous logic fits inside the executor, external resolve and reject only 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 ready
Premium

A concise answer to help you respond confidently on this topic during an interview.