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 ofPromise.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.