Promise.any Method in Javascript

In this post, I’ll try to share my learning around Promise.any method and AggregateError class which would be useful for understanding its usage and applying it for your specific requirement.

What Promise.any() does?

Promise.any takes an iterable of promises and returns a new promise that is fulfilled with the value of the first fulfilled promise in the iterable. If all promises reject, Promise.any rejects with an AggregateError containing all rejection reasons.

const promise1 = Promise.reject(new Error('First failed'));
const promise2 = new Promise((resolve) => setTimeout(resolve, 100, 'Success'));
const promise3 = Promise.reject(new Error('Second failed'));

Promise.any([promise1, promise2, promise3])
  .then((value) => console.log(value))
  .catch((error) => console.error(error));
// Output: "Success"

Use cases of Promise.any()

AggregateError Class

AggregateError is a new error class introduced alongside Promise.any. It is used to represent multiple errors in a single error object. When Promise.any rejects, it throws an AggregateError containing an array of errors from all rejected promises. It is used when:

const promise1 = Promise.reject(new Error('First failed'));
const promise2 = Promise.reject(new Error('Second failed'));
const promise3 = Promise.reject(new Error('Third failed'));

Promise.any([promise1, promise2, promise3])
  .then((value) => console.log(value))
  .catch((error) => {
    console.error('All promises rejected');
    console.error(error); // AggregateError with an array of errors
  });
// Output:
// All promises rejected
// AggregateError: All promises were rejected

Practical Use-cases of Promise.any()

  1. Trying multiple data sources: Suppose you have multiple sources providing the same data, and you want to use the data from the first source that succeeds. You can use Promise.any to get the data from the first successful source, ignoring any failed sources.

    const fetchFromSourceA = () => fetch('https://api.sourceA.com/data');
    const fetchFromSourceB = () => fetch('https://api.sourceB.com/data');
    const fetchFromSourceC = () => fetch('https://api.sourceC.com/data');
    
    Promise.any([fetchFromSourceA(), fetchFromSourceB(), fetchFromSourceC()])
      .then(async (response) => {
        const data = await response.json();
        console.log('Data fetched:', data);
      })
      .catch((error) => console.error('All sources failed:', error));
    // Output: Data fetched: { ... } from the first successful source
    
  2. Fallback strategies: Imagine you have a primary service and a backup service for providing data. You want to use data from the primary service, but if it fails, you want to use data from the backup service. You can use Promise.any to implement this fallback strategy.

    const fetchPrimaryData = () => fetch('https://api.primary.com/data');
    const fetchBackupData = () => fetch('https://api.backup.com/data');
    
    Promise.any([fetchPrimaryData(), fetchBackupData()])
      .then(async (response) => {
        const data = await response.json();
        console.log('Data fetched:', data);
      })
      .catch((error) =>
        console.error('Both primary and backup sources failed:', error),
      );
    // Output: Data fetched: { ... } from the primary or backup source
    
  3. Optimistic UI Updates: Suppose you have a web application that fetches data from a slow API. You want to update the UI as soon as possible using cached data, but you also want to fetch the latest data from the API. You can use Promise.any to update the UI with the first available data, either cached or fetched.

    function getLocalData() {
      // Implementation of getting cached data, which returns a Promise
      return Promise.resolve({ cached: 'data' });
    }
    function getRemoteData() {
      // Implementation of fetching data from the API, which returns a Promise
      return fetch('https://api.example.com/data');
    }
    Promise.any([getLocalData(), getRemoteData()])
      .then(async (response) => {
        const data =
          typeof response.json === 'function' ? await response.json() : response;
        console.log('Data fetched:', data);
      })
      .catch((error) => console.error('Failed to get data:', error));
    // Output: Data fetched: { ... } from the local cache or the remote API
    

Polyfill for Promise.any()

Promise.any() is supported in modern browsers and Node.js, but it might not be available in older environments. By creating a polyfill, we ensure that our code can run in environments that do not natively support Promise.any().

Implementation steps:

  1. Define the function that takes an array of promises.
  2. Create an executor function to handle promise resolution and rejection.
  3. Reject the case when the input array is empty.
  4. Iterate through the input promises and handle their rejected values in an array.
  5. Return a new promise that resolves if any input promise resolves and rejects as an AggregateError with an array of rejected values(errors) if all promises reject.
function any(promises) {
  const executorFunction = (resolve, reject) => {
    // base case
    if (promises.length === 0) {
      reject(new AggregateError('No promise was passed inside of Promise.any'));
      return;
    }
    const errors = [];
    let pendingCount = promises.length;
    promises.forEach((promise, idx) => {
      Promise.resolve(promise)
        .then((val) => resolve(val))
        .catch((err) => {
          errors[idx] = err;
          if (--pendingCount === 0) {
            reject(
              new AggregateError(
                'No Promise inside Promise.any resolved',
                errors,
              ),
            );
          }
        });
    });
  };
  return new Promise(executorFunction);
}