Promise.withResolvers(): The Deferred Pattern Built-In

Promise.withResolvers(): The Deferred Pattern Built-In

Promise.withResolvers() replaces the manual deferred pattern in JavaScript. One destructuring, no executor, no let. ES2024, supported in all modern runtimes.

Martin Ferret

Martin Ferret

June 23, 2026

For years, JavaScript developers wrote the same boilerplate to expose a promise's resolve and reject functions outside its constructor. It worked, but it always felt like fighting the API. ES2024 introduces Promise.withResolvers(), a small static method that folds this pattern into the language.

The Pattern Everyone Was Already Writing

When a promise is settled by something outside its executor, an event listener, a stream callback, a long-lived subscription, you need access to resolve and reject from the enclosing scope. The traditional pattern looks like this:

      let resolve, reject;
const promise = new Promise((res, rej) => {
  resolve = res;
  reject = rej;
});

// Now resolve/reject are accessible elsewhere
someEmitter.on('data', (value) => resolve(value));
someEmitter.on('error', (err) => reject(err));

    

It works, but several things are awkward. You need let declarations because the assignment happens inside the executor. The executor itself serves no purpose other than capturing the functions. And the same five lines reappear in codebase after codebase: React, Vue, Axios, TypeScript, Vite, and Deno's standard library all ship their own internal deferred() helper for this exact reason.

The New Form

Promise.withResolvers() returns an object with the three things you wanted in the first place:

      const { promise, resolve, reject } = Promise.withResolvers();

someEmitter.on('data', (value) => resolve(value));
someEmitter.on('error', (err) => reject(err));

return promise;

    

No let, no executor function, no closure dance. The promise and its controls live in the same scope, ready to be passed around or stored on this.

The method is fully equivalent to the manual pattern: it is sugar, not new semantics. But the sugar is the whole point: it eliminates an idiom that should never have needed to exist.

Where It Actually Helps

Three categories of code benefit the most.

Event-driven adapters

Wrapping an event emitter or callback API into a promise becomes a one-liner. No more nested executors, no more captured variables hovering above the promise creation.

      function waitForClick(element) {
  const { promise, resolve } = Promise.withResolvers();
  element.addEventListener('click', resolve, { once: true });
  return promise;
}

    

Queues and pools

When you build a connection pool, a job queue, or any structure that hands out a promise now and settles it later, you need to store resolve and reject somewhere. withResolvers makes the storage shape obvious:

      class RequestQueue {
  #pending = [];

  enqueue(payload) {
    const { promise, resolve, reject } = Promise.withResolvers();
    this.#pending.push({ payload, resolve, reject });
    this.#flush();
    return promise;
  }

  // resolve/reject called later, from #flush() or an error handler
}

    

Bridging streams and async iterables

MDN's own documentation uses this as the canonical example: turning a Node.js readable stream into an async iterable. Each batch of data gets its own promise, and a new one is created the moment the previous batch is consumed. The pattern is significantly cleaner with withResolvers because the resolve and reject functions are recreated naturally inside the loop, rather than reassigned through closure tricks.

What It Is Not

A few things worth being explicit about.

It is not a Deferred class. It returns a plain object with three fields. There is no .state, no .isResolved, no .then on the wrapper itself, only on the promise property.

It does not change Promise semantics. Calling resolve more than once still has no effect after the first call. Passing a promise to resolve still adopts that promise's eventual state. The same rules as new Promise() apply, because under the hood it is exactly that.

It does not eliminate the need for new Promise(). When the entire async logic fits naturally inside an executor, a single setTimeout, a one-off fetch wrapper — the constructor remains the right tool. withResolvers shines specifically when resolve and reject must live in a wider scope than the executor allows.

Where It Runs Today

Promise.withResolvers() is part of ES2024 and reached Baseline "newly available" in March 2024.

Runtimes. Node.js supports it natively from version 22 (April 2024). Node.js itself now uses it internally, its createDeferredPromise utility was replaced by Promise.withResolvers() in Node 22.12.

Browsers. Chrome and Edge from version 119 (October 2023). Firefox from 121 (December 2023). Safari from 17.4 (March 2024).

Tooling. TypeScript ships the type definitions in 5.4 and above (March 2024). For older runtimes, a polyfill is trivial, three lines that reproduce the exact equivalent shown in the MDN specification.

In practice. For a modern Angular, NestJS, React, or Vue project bundled with Vite or Webpack, you can use Promise.withResolvers() everywhere today. The one situation to watch: applications targeting older Safari versions (below 17.4) or older Node LTS (20 and below) still need a polyfill.

What to Take Away

Promise.withResolvers() is not a paradigm shift. It is the elimination of a paper cut that every JavaScript developer has encountered and worked around. The four-line manual pattern becomes one destructuring assignment. The intent of the code, I am creating a promise whose settlement happens elsewhere, is finally expressed directly.

For backend developers building queues, frontend developers wrapping event APIs, and library authors maintaining their own deferred() helper somewhere in utils.ts, this is the kind of small ergonomic win that adds up. Delete the helper, replace the call sites, ship the diff.

More certificates.dev articles

Get the latest news and updates on developer certifications. Content is updated regularly, so please make sure to bookmark this page or sign up to get the latest content directly in your inbox.