Jason Williams

Promise.allSettled reaches stage 2

Promises have been a household name in JavaScript for a few years now. They arrived in ECMAScript (6) 2015 and have been used heavily since.

The minimal functionality made it easier to get through specification and into the real world, but this can leave developers with limitations.

Promise combinator landscape

One common limitation I’ve come across is knowing when an array of promises have been settled whether they were fulfilled or rejected. If you were to use Promise.all() it would stop and return after the first rejection giving you a rejected promise; this is the job Promise.all was set out to do however getting the status and value from each promise is equally useful.

Promise.race() can have its uses but wouldn’t help us too much here as it would short circuit after the first promise is settled.

Here is an overview of the current and potential combinators available to the Promise constructor.

Name Description Initial Spec
Promise.allSettled does not short-circuit ES Proposal (Stage 2)
Promise.all short-circuits when an input value is rejected ES2015
Promise.race short-circuits when an input value is settled ES2015
Promise.any short-circuits when an input value is fulfilled ES Proposal

Motivation

Needing allSettled functionality is vital when dealing with multiple API fetches in a progressively enhanced application. For instance, on an article page the main content is useful, but you’re happy to disregard additional page furniture if it fails.

Being able to decide which promises are important required us to wrap current promises with extra logic. Workarounds involve looping through each promise, invoking the .then() and returning the result back into a new array. Here is an example:

function reflect(promise) {
return promise.then(
(v) => {
return { status: "fulfilled", value: v };
},
(error) => {
return { status: "rejected", reason: error };
}
);
}

const promises = [fetch("index.html"), fetch("https://does-not-exist/")];
const results = await Promise.all(promises.map(reflect));
const successfulPromises = results.filter((p) => p.status === "fulfilled");

And here is an example of the proposed solution in action:

const promises = [fetch("index.html"), fetch("https://does-not-exist/")];
const results = await Promise.allSettled(promises);
const successfulPromises = results.filter((p) => p.status === "fulfilled");

Above we pass 2 fetch promises into allSettled. This returns to us an array of objects giving us the “status” and “value”, or “status” and “reason” if its failed. It’s now easy for us to filter out any rejected promises.

In 2018 I started putting together an ECMAScript proposal to have this in the specification, I’ve had great help from @Mathias Bynens who then went on to champion it at TC39. After passing stage 1 he’s been helping myself and Rob Pamely through the standardisation and spec writing process.

I won’t lie, writing spec text was difficult, between the 3 of us we managed to work our way through.

We say that a promise is settled if it is not pending, i.e. if it is either fulfilled or rejected

States and Fates – Domenic Denicola

If you wish to understand more of the terminology @Domenic put together a great doc called “States and Fates”. whenever I thought I was getting lost with Promises, I would give this a read.

Whats next?

Promise.allSettled is now sitting at Stage 2 which means we have a first version of what will be in the specification.

We will need to spend more time getting feedback on the draft specification, naming, compatibility and any other concerns which appear. Experimental implementations will also be needed, core.js has Promise.allSettled() implemented and you can use this today. As always be weary that implementations can change as specifications do during the stages.

If you’re interested in following the process or contributing, you can view the allSettled repository here: https://github.com/tc39/proposal-promise-allSettled

Questions or comments about this API? Let me know on twitter @jason_williams