Clean Up Async Code with JavaScript Promise.withResolvers()

Problem

Sometimes you need to create a Promise manually — wrapping event-based APIs, converting timers, or building deferred patterns.

The traditional approach looks like this:

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

// Use resolve/reject externally
someEmitter.on('done', resolve);
someEmitter.on('error', reject);

Declaring let variables and assigning them inside a callback just to extract resolve and reject. It works, but it’s awkward.

Solution

Promise.withResolvers() does this in a single line:

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

someEmitter.on('done', resolve);
someEmitter.on('error', reject);

You get promise, resolve, and reject all at once via destructuring.

Here’s a practical pattern for request-response over WebSockets:

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

  const timeout = setTimeout(() => {
    reject(new Error('Request timeout'));
  }, 5000);

  return {
    promise,
    complete(data) {
      clearTimeout(timeout);
      resolve(data);
    },
    fail(error) {
      clearTimeout(timeout);
      reject(error);
    }
  };
}

// Usage
const req = createDeferredRequest();
ws.send(message);
ws.onmessage = (e) => req.complete(JSON.parse(e.data));
const result = await req.promise;

Key Points

  • Promise.withResolvers() returns a { promise, resolve, reject } object
  • No constructor callback needed — you get resolve/reject references directly
  • Supported in Chrome 119+, Firefox 121+, Safari 17.4+, Node.js 22+
  • Perfect for any scenario requiring the Deferred pattern