Implementing Promise.any in JavaScript


Let's dive into implementing Promise.any, a method introduced in ECMAScript 2021. This method takes an iterable of Promise objects and, as soon as one of the promises in the iterable fulfills, returns a single promise that resolves with the value from that promise. If no promises in the iterable fulfill (i.e., if all of them reject), then the returned promise is rejected with an AggregateError, a new subclass of Error that groups together multiple errors.


What is Promise.any?

Promise.any is designed to handle cases where you want to resolve as soon as one of the provided promises succeeds. It is useful when you have multiple asynchronous operations and you only need one to succeed to proceed. Unlike Promise.all, which requires all promises to resolve, Promise.any only requires one.

Real Interview Insights

Interviewers might ask you to:

  • Implement Promise.any from scratch.
  • Explain the behavior of Promise.any in various scenarios.
  • Discuss how Promise.any handles errors and compare it to Promise.all and Promise.race.

Implementing Promise.any

Here’s a basic implementation of Promise.any:

function promiseAny(promises) {
  return new Promise((resolve, reject) => {
    if (!Array.isArray(promises)) {
      throw new TypeError('Input must be an array');
    }
 
    const errors = [];
    const promisesCount = promises.length;
    let rejectionCount = 0;
 
    if (promisesCount === 0) {
      return reject(new AggregateError([], 'All promises were rejected'));
    }
 
    promises.forEach(promise => {
      Promise.resolve(promise)
        .then(resolve)
        .catch(error => {
          errors.push(error);
          rejectionCount++;
 
          if (rejectionCount === promisesCount) {
            reject(new AggregateError(errors, 'All promises were rejected'));
          }
        });
    });
  });
}
Explanation:
  • Input Validation: Ensure the input is an array. If not, throw a TypeError.
  • Error Tracking: Track errors in an array. If all promises reject, use AggregateError to aggregate and report all errors.
  • Promise Processing: Use Promise.resolve to handle both promise and non-promise values. Resolve with the first fulfilled promise and reject if all promises are rejected.

Practical Example

Consider an example with multiple promises:

const promise1 = Promise.reject('Error 1');
const promise2 = Promise.reject('Error 2');
const promise3 = Promise.resolve('Success 3');
 
promiseAny([promise1, promise2, promise3])
  .then(result => console.log(result)) // Output: 'Success 3'
  .catch(error => console.error(error));

In this example:

  • The promiseAny function returns the result of promise3 as it is the first promise to fulfill, ignoring the rejections from promise1 and promise2.

Advanced Use Case: Handling Empty Arrays

To handle empty arrays gracefully, ensure your implementation checks for this case and rejects with an appropriate error:

function promiseAny(promises) {
  return new Promise((resolve, reject) => {
    if (!Array.isArray(promises)) {
      throw new TypeError('Input must be an array');
    }
 
    const errors = [];
    const promisesCount = promises.length;
    let rejectionCount = 0;
 
    if (promisesCount === 0) {
      return reject(new AggregateError([], 'All promises were rejected'));
    }
 
    promises.forEach(promise => {
      Promise.resolve(promise)
        .then(resolve)
        .catch(error => {
          errors.push(error);
          rejectionCount++;
 
          if (rejectionCount === promisesCount) {
            reject(new AggregateError(errors, 'All promises were rejected'));
          }
        });
    });
  });
}

Performance Considerations

When implementing Promise.any, consider:

  • Error Handling: Aggregating errors can be resource-intensive with many rejected promises.
  • Early Resolution: The function resolves as soon as the first promise fulfills, which is efficient but may not be suitable for all use cases.

Coding Challenge: Enhancing Promise.any with Timeout

Challenge: Enhance the promiseAny function to include a timeout mechanism, allowing it to reject if no promises are fulfilled within a specified time limit.

function promiseAny(promises, timeout = 5000) {
  return new Promise((resolve, reject) => {
    if (!Array.isArray(promises)) {
      throw new TypeError('Input must be an array');
    }
 
    const errors = [];
    const promisesCount = promises.length;
    let rejectionCount = 0;
    let timeoutId;
 
    if (promisesCount === 0) {
      return reject(new AggregateError([], 'All promises were rejected'));
    }
 
    if (timeout) {
      timeoutId = setTimeout(() => {
        reject(new Error('Operation timed out'));
      }, timeout);
    }
 
    promises.forEach(promise => {
      Promise.resolve(promise)
        .then(result => {
          clearTimeout(timeoutId);
          resolve(result);
        })
        .catch(error => {
          errors.push(error);
          rejectionCount++;
 
          if (rejectionCount === promisesCount) {
            clearTimeout(timeoutId);
            reject(new AggregateError(errors, 'All promises were rejected'));
          }
        });
    });
  });
}
 
// Example usage with timeout
const promise1 = new Promise((resolve, reject) => setTimeout(reject, 6000, 'Error 1'));
const promise2 = new Promise((resolve, reject) => setTimeout(reject, 3000, 'Error 2'));
const promise3 = new Promise((resolve) => setTimeout(resolve, 2000, 'Success 3'));
 
promiseAny([promise1, promise2, promise3], 5000)
  .then(result => console.log(result))
  .catch(error => console.error(error));

In this challenge:

  • Add a timeout to reject the promise if no promises fulfill within the specified time.

Implementing Promise.any from scratch provides insight into handling multiple promises with a focus on the first successful resolution. Mastering this technique helps in managing complex asynchronous scenarios and prepares you for technical interviews.