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 Promises. However, unlike those other two methods, Promise.any() will resolve as soon as any of the Promises 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 Promises 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 Promises, 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 Promises in the iterable resolve, or it will reject asynchronously if all of the Promises 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 Promises:

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 Promises around it. Even though promise3 is the first to reject, Promise.any() will continue looking through all of its passed Promises 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 Promises 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 Promises 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 Promises 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 Promises:

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 Promises in the iterable fulfills, with the value of the fulfilled Promise. If no Promises in the iterable fulfill (if all of the given Promises 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.