Promise.any()
Today's post features something that is both part of the Promise
ecosystem that I've been writing about and a feature that is new with ES2021, called Promise.any()
. Like Promise.all()
and Promise.allSettled()
, Promise.any()
accepts an iterable of Promise
s. However, unlike those other two methods, Promise.any()
will resolve as soon as any of the Promise
s in the iterable fulfills, with the value of that fulfilled Promise
.
This behavior gives us a type of short circuit behavior, where the first Promise
to fulfill out of the iterable will resolve the whole method, regardless of any other Promises
in the iterable that may be waiting. This behavior is useful if we only need one Promise
to fulfill but it doesn't matter which one. A good example would be loading a random image from a library. We don't care which image shows up, we just want one to appear and load as quickly as possible.
We have seen short circuit behavior before in Promise.all()
, but in an opposite sort of fashion in which that method will short circuit as soon as any of the provided Promise
s reject, giving us a fail fast kind of short circuiting.
Similar short circuit behavior is also observed in Promise.race()
, which returns the first settled value – that is, the first fulfilled or rejected Promise
from its iterable. Promise.any()
focuses solely on the first fulfilled Promise
.
Syntax
Promise.any(iterable);
Parameters
iterable
- An iterable object such as an Array.
Return Value
If Promise.any()
is passed an empty iterable, then it will return an already rejected Promise
. If it is passed an iterable that doesn't contain any Promise
s, then it will return an asynchronously resolved Promise
. In all other cases, it will return a pending Promise
. This Promise
is then resolved asynchronously if any of the Promise
s in the iterable resolve, or it will reject asynchronously if all of the Promise
s in the iterable reject.
Unlike Promise.all()
, which returns an array of fulfillment values, or Promise.allSettled()
, which returns an array of outcome objects, Promise.any()
only returns a single fulfillment value (assuming at least one Promise
from the iterable fulfills).
The other difference (that may seem odd at first) is in the fact that this method rejects if it is given an empty iterable. Contrast this with Promise.all()
, which resolves if it is given an empty iterable. This happens because an empty iterable contains no items that fulfill. Back to our example of grabbing a random image from a library, an empty iterable would not deliver an image, so you could not rely on this method to deliver "any image", so a rejection here makes sense.
Fulfillment
const promise1 = Promise.resolve("first promise done");const promise2 = "second item done"; // not a Promise!const promise3 = new Promise((resolve, reject) => { setTimeout(() => resolve("third promise done"), 2000);});
Promise.any([promise1, promise2, promise3]).then((value) => { console.log(value);});// expected result (immediately):// "first promise done"
We've looked at this example a few times across Promise.all()
and Promise.allSettled()
. In this case, the very first Promise
from the iterable, promise1
, resolves immediately, so its resolved value, "first promise done"
is what Promise.any()
returns.
Let's take a look at what happens when the first item that would resolve is a non-Promise
:
const promise1 = new Promise((resolve, reject) => { setTimeout(() => resolve("first promise done"), 1000);});const promise2 = "second item done"; // not a Promise!const promise3 = new Promise((resolve, reject) => { setTimeout(() => resolve("third promise done"), 2000);});
Promise.any([promise1, promise2, promise3]).then((value) => { console.log(value);});// expected result (immediately):// "second item done"
I've modified the previous example so that the promise1
is now held up by a one second setTimeout
function, which makes promise2
, the poorly-named non-Promise
item, the first to resolve. When we run this code, Promise.any()
returns "second item done"
, demonstrating that it still returns the first settled item out of the given iterable, even if that item is not actually a Promise
.
In fact, let's take a look at the odd scenario when none of the items in the iterable are Promise
s:
const nonpromise1 = "first item done";const nonpromise2 = 123456;const nonpromise3 = false;
Promise.any([nonpromise1, nonpromise2, nonpromise3]).then((value) => { console.log(value);});// expected result (immediately):// "first item done"
In this example, the iterable contains a String, a number, and a boolean – all non-Promise
values. Promise.any()
returns the value of nonpromise1
, "first item done"
immediately.
Now let's toss in a rejection just to see how that gets handled:
const promise1 = new Promise((resolve, reject) => { setTimeout(() => resolve("first promise done"), 1000);});const promise2 = new Promise((resolve, reject) => { setTimeout(() => resolve("second promise done"), 2000);});const promise3 = new Promise((resolve, reject) => { setTimeout(() => reject(new Error("reject!!"), 500));});const promise4 = new Promise((resolve, reject) => { setTimeout(() => resolve("fourth promise done"), 4000);});
Promise.any([promise1, promise2, promise3, promise4]).then( (value) => console.log(value), (error) => console.error(error));// expected result (after one second): "first promise done"
I've added in a rejection to promise3
, and just to demonstrate how Promise.any()
behaves, I've made this rejection run on a setTimeout
function that is faster than any of the resolved Promise
s around it. Even though promise3
is the first to reject, Promise.any()
will continue looking through all of its passed Promise
s for any successful resolution. As such, it moves from promise3
to promise1
, which resolves after one second, and returns "first promise done"
.
Rejection
So even if there are rejected Promise
s in the iterable, as long as there is one resolved Promise
, Promise.any()
will return a value.
As such, there are two instances in which Promise.any()
will actually reject. The first is if all of the passed in Promise
s reject:
const promise1 = new Promise((resolve, reject) => { setTimeout(() => { try { throw new Error("first promise rejected"); } catch (e) { reject(e); } }, 1000);});
const promise2 = new Promise((resolve, reject) => { setTimeout(() => { try { throw new Error("second promise rejected"); } catch (e) { reject(e); } }, 2000);});
const promise3 = new Promise((resolve, reject) => { setTimeout(() => { try { throw new Error("third promise rejected"); } catch (e) { reject(e); } }, 500);});
const promise4 = new Promise((resolve, reject) => { setTimeout(() => { try { throw new Error("fourth promise rejected"); } catch (e) { reject(e); } }, 4000);});
Promise.any([promise1, promise2, promise3, promise4]).then( (value) => console.log(value), (error) => console.error(error));// expected result (after four seconds): [AggregateError: All promises were rejected]
This example looks a little bit different from the examples above, but it's basically the same. We just need to wrap the Error
inside of the setTimeout
function in a try-catch
statement so that the time delays still work as intended when we run this code.
At any rate, all four of these Promise
s reject, and after four seconds Promise.any()
rejects with an AggregateError
. This is a new type of Error in JavaScript, which represents several errors wrapped in a single Error. I won't get too deep into AggregateError
in this post, but if you're curious you can read more in the AggregateError
article on MDN. This error does have a number of useful properties that will help you debug all of your failed Promise
s:
const promise1 = new Promise((resolve, reject) => { setTimeout(() => { try { throw new Error("first promise rejected"); } catch (e) { reject(e); } }, 1000);});
const promise2 = new Promise((resolve, reject) => { setTimeout(() => { try { throw new Error("second promise rejected"); } catch (e) { reject(e); } }, 2000);});
const promise3 = new Promise((resolve, reject) => { setTimeout(() => { try { throw new Error("third promise rejected"); } catch (e) { reject(e); } }, 500);});
const promise4 = new Promise((resolve, reject) => { setTimeout(() => { try { throw new Error("fourth promise rejected"); } catch (e) { reject(e); } }, 4000);});
Promise.any([promise1, promise2, promise3, promise4]).catch((error) => { console.error(error); // [AggregateError: All promises were rejected] console.log(error instanceof AggregateError); // true console.log(error.message); // "All promises were rejected" console.log(error.name); // "AggregateError" console.log(error.errors); // ["Error: first promise rejected", "Error: second promise rejected", "Error: third promise rejected", "Error: fourth promise rejected"]});
The second instance in which Promise.any()
rejects is if the iterable it is passed is empty:
Promise.any([]).then( (value) => console.log(value), (error) => { console.error(error); // [AggregateError: All promises were rejected] console.log(error.errors); // [] });
In this case, the errors
property within the AggregateError
object is an empty array.
From MDN: Promise.any()
takes an iterable of Promise
objects. It returns a single Promise
that resolves as soon as any of the Promise
s in the iterable fulfills, with the value of the fulfilled Promise
. If no Promise
s in the iterable fulfill (if all of the given Promise
s are rejected), then the returned Promise
is rejected with an AggregateError
, a new subclass of Error
that groups together individual errors.
See more examples and further documentation here.