Skip to content
This repository has been archived by the owner on Dec 12, 2023. It is now read-only.

tc39/proposal-promise-with-resolvers

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

38 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Promise.withResolvers

Note: this proposal is now at stage 4. See the spec PR here: tc39/ecma262#3179

Status

Stage: 4

Champions:

Authors:

Stage 3 slides Stage 2 slides Stage 1 slides

Synopsis

When hand-rolling a Promise, the user must pass an executor callback which takes two arguments: a resolve function, which triggers resolution of the promise, and a reject function, which triggers rejection. This works well if the callback can embed a call to an asynchronous function which will eventually trigger the resolution or rejection, e.g., the registration of an event listener.

const promise = new Promise((resolve, reject) => {
  asyncRequest(config, response => {
    const buffer = [];
    response.on('data', data => buffer.push(data));
    response.on('end', () => resolve(buffer));
    response.on('error', reason => reject(reason));
  });
});

Often however developers would like to configure the promise's resolution and rejection behavior after instantiating it. Today this requires a cumbersome workaround to extract the resolve and reject functions from the callback scope:

let resolve, reject;
const promise = new Promise((res, rej) => {
  resolve = res;
  reject = rej;
});
asyncRequest(config, response => {
  const buffer = [];
  response.on('callback-request', id => {
    promise.then(data => callback(id, data));
  });
  response.on('data', data => buffer.push(data));
  response.on('end', () => resolve(buffer));
  response.on('error', reason => reject(reason));
});

Developers may also have requirements that necessitate passing resolve/reject to more than one caller, so they MUST implement it this way:

let resolve = () => { };
let reject = () => { };

function request(type, message) {
  if (socket) {
    const promise = new Promise((res, rej) => {
      resolve = res;
      reject = rej;
    });
    socket.emit(type, message);
    return promise;
  }

  return Promise.reject(new Error('Socket unavailable'));
}

socket.on('response', response => {
  if (response.status === 200) {
    resolve(response);
  }
  else {
    reject(new Error(response));
  }
});

socket.on('error', err => {
  reject(err);
});

This is boilerplate code that is very frequently re-written by developers. This proposal simply seeks to add a static method, tentatively called withResolvers, to the Promise constructor which returns a promise along with its resolution and rejection functions conveniently exposed.

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

This method or something like it may be known to some committee members under the name defer or deferred, names also sometimes applied to utility functions in the ecosystem. This proposal adopts a more descriptive name for the benefit of users who may not be familiar with those historical functions.

Existing implementations

Libraries and applications continually re-invent this wheel. Below are just a handful of examples.

Library Example
React inline example
Vue inline example
Axios inline example
TypeScript utility
Vite inline example
Deno stdlib utility