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 toPromise.all
andPromise.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 ofpromise3
as it is the first promise to fulfill, ignoring the rejections frompromise1
andpromise2
.
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.