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.

promise with resolvers

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 instance
  • resolve : Function to fulfill the promise
  • reject : 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.