What Interviewers Ask Today: Implementing Promise.any in JavaScript

In modern JavaScript development, understanding Promises and asynchronous programming is crucial. A common interview question you might encounter is: "Can you implement the Promise.any function?" In this article, we'll dive deep into how to build your own version of Promise.any, exploring key concepts and patterns along the way.

Understanding Promise.any

Before we start coding, let's clarify what Promise.any does:

  • Purpose: It takes an array of Promises and returns a new Promise.
  • Behavior: The returned Promise fulfills as soon as any of the input Promises fulfills.
  • Rejection: If all input Promises reject, it rejects with an AggregateError, which contains all the rejection reasons.

Setting Up the Problem

Here's the skeleton of the function we need to implement:

function promiseAny(promises) {
  // Your code here
}

We'll also use some helper functions to simulate Promises that resolve or reject after a certain time:

function resolveAfter(ms, value) {
  return new Promise((resolve) => setTimeout(() => resolve(value), ms))
}

function rejectAfter(ms, error) {
  return new Promise((_, reject) => setTimeout(() => reject(error), ms))
}

And here's how we'll test our promiseAny function:

const promise1 = resolveAfter(300, 'first')
const promise2 = rejectAfter(50, 'second failed')
const promise3 = resolveAfter(100, 'third')
const promise4 = rejectAfter(400, 'fourth failed')

promiseAny([promise1, promise2, promise3, promise4])
  .then((result) => console.log('Result:', result))
  .catch((error) => console.error('Error:', error))

Implementing promiseAny

Now, let's build our own promiseAny step by step.

Step 1: Create a New Promise

Our promiseAny function should return a new Promise:

function promiseAny(promises) {
  return new Promise((resolve, reject) => {
    // Implementation will go here
  })
}

Step 2: Handle Empty Input

If the input array is empty, the Promise should reject immediately with an AggregateError:

if (promises.length === 0) {
  return reject(new AggregateError([], 'All promises were rejected'))
}

Step 3: Initialize Variables

We'll need to keep track of the number of rejections and collect the errors:

let rejectionCount = 0
const errors = []

Step 4: Iterate Over Promises

We'll loop through each Promise and attach then and catch handlers:

promises.forEach((promise, index) => {
  Promise.resolve(promise)
    .then((value) => {
      resolve(value)
    })
    .catch((error) => {
      rejectionCount++
      errors[index] = error
      if (rejectionCount === promises.length) {
        reject(new AggregateError(errors, 'All promises were rejected'))
      }
    })
})

Full Implementation

Putting it all together:

function promiseAny(promises) {
  return new Promise((resolve, reject) => {
    if (promises.length === 0) {
      return reject(new AggregateError([], 'All promises were rejected'))
    }

    let rejectionCount = 0
    const errors = []

    promises.forEach((promise, index) => {
      Promise.resolve(promise)
        .then((value) => {
          resolve(value)
        })
        .catch((error) => {
          rejectionCount++
          errors[index] = error
          if (rejectionCount === promises.length) {
            reject(new AggregateError(errors, 'All promises were rejected'))
          }
        })
    })
  })
}

Testing the Function

Now, when we run our test code:

promiseAny([promise1, promise2, promise3, promise4])
  .then((result) => console.log('Result:', result))
  .catch((error) => console.error('Error:', error))

We get the output:

Result: third

Even though promise2 rejects first, promise3 fulfills shortly after, and since promiseAny resolves as soon as any Promise fulfills, we get 'third' as the result.

Deep Dive into the Implementation

Using Promise.resolve()

We wrap each input in Promise.resolve() to handle cases where the input might not be a genuine Promise but still a thenable (an object with a then method).

Handling Rejections with an AggregateError

If all Promises reject, we create a new AggregateError, which is an array-like object containing all individual errors:

reject(new AggregateError(errors, 'All promises were rejected'))

The Importance of Indexing Errors

By storing errors with their corresponding indices, we maintain the order of the errors as they relate to the input Promises.

Common Mistakes to Avoid

  • Not Handling Non-Promise Inputs: Always use Promise.resolve() to normalize inputs.
  • Forgetting to Check for Empty Arrays: An empty input should immediately reject.
  • Not Using AggregateError: Simply rejecting with a single error doesn't meet the specification of Promise.any.

Conclusion

Implementing Promise.any is an excellent way to demonstrate your understanding of Promises and asynchronous patterns in JavaScript. It tests your ability to handle multiple asynchronous operations and error handling gracefully.

Remember, the key points are:

  • Resolve as soon as any Promise fulfills.
  • Reject only if all Promises reject.
  • Collect and return all errors in an AggregateError.

By mastering this implementation, you'll be well-prepared for similar questions in your next JavaScript interview.