Promise.withResolvers()
In this blog post we take a look at the ECMAScript 2024 feature “Promise.withResolvers
” (proposed by Peter Klecha). It provides a new way of directly creating Promises, as an alternative to new Promise(...)
.
new Promise(...)
– the revealing constructor pattern Before Promise.withResolvers()
, there was only one way to create Promises directly – via the following pattern:
const promise = new Promise(
(resolve, reject) => {
// ···
}
);
Quoting Domenic Denicola, one of the people behind JavaScript’s Promise API:
I call this the revealing constructor pattern because the
Promise
constructor is revealing its internal capabilities, but only to the code that constructs the promise in question. The ability to resolve or reject the promise is only revealed to the constructing code, and is crucially not revealed to anyone using the promise. So if we hand offp
to another consumer, say
doThingsWith(p);
then we can be sure that this consumer cannot mess with any of the internals that were revealed to us by the constructor. This is as opposed to, for example, putting
resolve
andreject
methods onp
, which anyone could call.
As an example, let’s convert a callback-based function into a Promise-based one (note that Node.js does have a complete Promise-based API, node:fs/promises
).
The following code shows what using the callback-based function fs.readFile()
looks like:
import * as fs from 'node:fs';
fs.readFile('some-file.txt', 'utf-8', (error, result) => {
if (error !== null) {
console.error(error);
return;
}
assert.equal(
result,
'Content of some-file.txt'
);
});
Let’s implement a Promise-based version of fs.readFile()
:
import * as fs from 'node:fs';
function readFileAsync(filePath, encoding) {
return new Promise(
(resolve, reject) => {
fs.readFile(filePath, encoding, (error, result) => {
if (error !== null) {
reject(error);
return;
}
resolve(result);
});
}
);
}
assert.equal(
await readFileAsync('some-file.txt', 'utf-8'),
'Content of some-file.txt'
);
Promise.withResolvers()
One limitation of the revealing constructor pattern is that the settlement functions resolve
and reject
can’t leave the Promise
constructor callback and be used separately from the Promise. That is fixed via the following static factory method:
const { promise, resolve, reject } = Promise.withResolvers();
This is what using that factory method looks like:
{
const { promise, resolve, reject } = Promise.withResolvers();
resolve('fulfilled');
assert.equal(
await promise,
'fulfilled'
);
}
{
const { promise, resolve, reject } = Promise.withResolvers();
reject('rejected');
try {
await promise;
} catch (err) {
assert.equal(err, 'rejected');
}
}
We can implement Promise.withResolvers()
as follows:
function promiseWithResolvers() {
let resolve;
let reject;
const promise = new Promise(
(res, rej) => {
// Executed synchronously!
resolve = res;
reject = rej;
});
return {promise, resolve, reject};
}
The proposal points out how many code bases implement this functionality (which is why it is good news that it is now built into the language): React, Vue, Axios, TypeScript, Vite, Deno’s standard library.
Let’s revisit our previously implemented function readFileAsync()
. With the new API, we can write it as follows:
import * as fs from 'node:fs';
function readFileAsync(filePath, encoding) {
const { promise, resolve, reject } = Promise.withResolvers();
fs.readFile(filePath, encoding, (error, result) => {
if (error !== null) {
reject(error);
return;
}
resolve(result);
});
return promise;
}
That code is still more or less the same as the one where we used the Promise
constructor. Let’s move on to use cases that the constructor can‘t handle.
class OneElementQueue {
#promise = null;
#resolve = null;
constructor() {
const { promise, resolve } = Promise.withResolvers();
this.#promise = promise;
this.#resolve = resolve;
}
get() {
return this.#promise;
}
put(value) {
this.#resolve(value);
}
}
{ // Putting before getting
const queue = new OneElementQueue();
queue.put('one');
assert.equal(
await queue.get(),
'one'
);
}
{ // Getting before putting
const queue = new OneElementQueue();
setTimeout(
// Runs after `await` pauses the current execution context
() => queue.put('two'),
0
);
assert.equal(
await queue.get(),
'two'
);
}
PromiseQueue
is a potentially infinite queue:
.get()
blocks until a value is available..put(value)
is non-blockingThe code is a slight rewrite of function makeQueue()
in package stream
of Endo, a distributed secure JavaScript sandbox, based on SES. Check out that package for more code that uses makePromiseKit()
– which is the equivalent of Promise.withResolvers()
.
class PromiseQueue {
#frontPromise;
#backResolve;
constructor() {
const { promise, resolve } = Promise.withResolvers();
this.#frontPromise = promise;
this.#backResolve = resolve;
}
put(value) {
const { resolve, promise } = Promise.withResolvers();
// By resolving, we add another (pending) element
// to the end of the queue
this.#backResolve({ value, promise });
this.#backResolve = resolve;
}
get() {
return this.#frontPromise.then(
(next) => {
this.#frontPromise = next.promise;
return next.value;
}
);
}
}
{ // Putting before getting
const queue = new PromiseQueue();
queue.put('one');
queue.put('two');
assert.equal(
await queue.get(),
'one'
);
assert.equal(
await queue.get(),
'two'
);
}
{ // Getting before putting
const queue = new PromiseQueue();
setTimeout(
// Runs after `await` pauses the current execution context
() => {
queue.put('one');
queue.put('two');
},
0
);
assert.equal(
await queue.get(),
'one'
);
assert.equal(
await queue.get(),
'two'
);
}
Each queue element is a Promise for {value, promise}
:
value
is the value stored in the queue element.promise
is the next (potentially pending) queue element.Front and back of the queue:
resolve
function for the last (pending!) queue element.class AsyncIterQueue {
#frontPromise;
#backResolve;
constructor() {
const { promise, resolve } = Promise.withResolvers();
this.#frontPromise = promise;
this.#backResolve = resolve;
}
put(value) {
if (this.#backResolve === null) {
throw new Error('Queue is closed');
}
const { resolve, promise } = Promise.withResolvers();
this.#backResolve({ done: false, value, promise });
this.#backResolve = resolve;
}
close() {
this.#backResolve(
{ done: true, value: undefined, promise: null }
);
this.#backResolve = null;
}
next() {
if (this.#frontPromise === null) {
return Promise.resolve({done: true});
}
return this.#frontPromise.then(
(next) => {
this.#frontPromise = next.promise;
return {value: next.value, done: next.done};
}
);
}
[Symbol.asyncIterator]() {
return this;
}
}
{ // Putting before async iteration
const queue = new AsyncIterQueue();
queue.put('one');
queue.put('two');
queue.close();
assert.deepEqual(
await Array.fromAsync(queue),
['one', 'two']
);
}
{ // Async iteration before putting
const queue = new AsyncIterQueue();
setTimeout(
// Runs after `await` pauses the current execution context
() => {
queue.put('one');
queue.put('two');
queue.close();
},
0
);
assert.deepEqual(
await Array.fromAsync(queue),
['one', 'two']
);
}
Not much has changed compared to the previous implementation:
.next()
and .[Symbol.asyncIterator]()
implement the AsyncIterable interface.{value, done, promise}
..close()
lets us close queues, by adding a final element to the queue:{ done: true, value: undefined, promise: null }
Promise.deferred()
(or Promise.defer()
)? The names “deferred” only make sense to people who are aware of the history of Promises: It was a name that was used in jQuery’s Promise API. If you are new to JavaScript that name doesn’t mean anything to you. [Source]
Resolving a Promise via resolve()
only means that its fate is determined:
const {promise, resolve} = Promise.withResolvers();
resolve(123); // settles `promise`
const {promise, resolve} = Promise.withResolvers();
resolve(new Promise(() => {})); // `promise` is forever pending
Thus, resolve
and reject
generally only resolve Promises – they don’t always settle them. [Source]
Furthermore, the ECMAScript specification uses the name “resolving functions” for resolve
and reject
.
This concludes our excursion into the world of Promises. If you want to know more about asynchronous programming in JavaScript, you can read the following chapters of my book “JavaScript for impatient programmers”: