Promise.all Polyfill
Implement a promiseAll function that mimics the behavior of Promise.all.
Function Signature:
const promiseAll = (promises) => { // Your implementation };
Key Concepts:
- Concurrent Execution: All Promises start executing immediately, not sequentially
- Order Preservation: Results must match input order, regardless of which Promise completes first
- Fail-Fast: The first rejection should immediately reject the entire operation
Examples
const promises1 = [
Promise.resolve(1),
Promise.resolve(2),
Promise.resolve(3)
];
promiseAll(promises1)
.then(val => console.log('Result:', val))
.catch(err => console.error('Error:', err));Result: [1, 2, 3]const promises2 = [
Promise.resolve(1),
Promise.resolve(2),
Promise.reject('Error occurred')
];
promiseAll(promises2)
.then(val => console.log('Result:', val))
.catch(err => console.error('Error:', err));Error: Error occurredConstraints
- The input is an array of Promises or any values
- Non-Promise values should be treated as already-resolved Promises
- Empty arrays should resolve to an empty array
- Results must maintain the input order
Notes
- Use `Promise.resolve()` to convert non-Promise values
- Track results by index to maintain order
- Reject immediately when any Promise rejects (don't wait for others)
Hints
Editorial: Promise.all Polyfill
Understanding Promise.all
Promise.all is a powerful utility method that executes multiple Promises concurrently and waits for all of them to complete. It returns a single Promise that resolves with an array of results in the same order as the input Promises, or rejects immediately if any Promise rejects (fail-fast behavior).
The Problem
When working with multiple asynchronous operations, you often need to:
- Execute them concurrently (not sequentially) for better performance
- Wait for all of them to complete before proceeding
- Maintain the order of results matching the input order
- Handle errors gracefully with fail-fast behavior
Promise.all solves all these requirements elegantly.
Implementation Strategy
- Return a new Promise: Wrap the entire logic in a Promise constructor
- Handle empty array: Immediately resolve with an empty array
- Track completion: Use a counter to track how many Promises have resolved
- Preserve order: Store results in an array using the original index
- Fail-fast: Reject immediately when any Promise rejects
- Handle non-Promises: Convert all values to Promises using
Promise.resolve()
Solution Code
const promiseAll = (promises) => { const results = []; let count = 0; return new Promise((resolve, reject) => { // Handle empty array edge case if (promises.length === 0) { resolve([]); return; } // Iterate through all promises promises.forEach((promise, index) => { // Convert non-Promise values to Promises Promise.resolve(promise) .then((data) => { // Store result at the correct index to preserve order results[index] = data; count++; // Check if all promises have resolved if (count === promises.length) { resolve(results); } }) .catch(reject); // Fail-fast: reject immediately on any error }); }); };
Key Points
Order Preservation:
By placing each resolved value into results[index], we ensure that outputs appear in the same order as their input—no matter which Promise finishes first.
Fail-Fast Behavior:
Using .catch(reject) causes our promise to reject instantly if any input fails. We don't wait for any other Promises.
Promise.resolve():
This method wraps ordinary values (such as numbers, strings, or objects) in resolved Promises, letting us handle arrays that mix Promises and non-Promises seamlessly.
Counter Pattern:
A count variable increments as values resolve. When count === promises.length, we know all have completed, so we resolve with the full results array.
Concurrent Execution:
All Promises begin (or values are wrapped and inserted) at once since forEach applies instantly. This is true concurrency, not waiting for one before starting the next.
Step-by-Step Execution Flow
Let's trace through an example:
const promises = [ Promise.resolve(1), Promise.resolve(2), Promise.resolve(3) ]; promiseAll(promises).then(console.log); // [1, 2, 3]
- Initialization:
results = [],count = 0 - Iteration: For each promise:
- Promise at index 0: starts executing, will resolve with 1
- Promise at index 1: starts executing, will resolve with 2
- Promise at index 2: starts executing, will resolve with 3
- Asynchronous Resolution (order may vary):
- Promise 2 resolves first:
results[2] = 3,count = 1 - Promise 0 resolves:
results[0] = 1,count = 2 - Promise 1 resolves:
results[1] = 2,count = 3
- Promise 2 resolves first:
- Completion:
count === promises.length, so we resolve with[1, 2, 3]
Notice how the results array maintains order even though Promise 2 resolved first!
Handling Rejections (Fail-Fast)
const promises = [ Promise.resolve(1), Promise.resolve(2), Promise.reject('Error') ]; promiseAll(promises) .then(console.log) .catch(console.error); // 'Error'
When the third Promise rejects:
- The
.catch(reject)immediately callsreject('Error') - The entire Promise.all rejects, even though Promises 1 and 2 might still be pending
- This is the fail-fast behavior - we don't wait for other Promises to complete
Handling Non-Promise Values
const promises = [ Promise.resolve(1), 42, // Regular number Promise.resolve(3) ]; promiseAll(promises).then(console.log); // [1, 42, 3]
Promise.resolve(42) converts the number into a resolved Promise, so it works seamlessly with actual Promises.
Time Complexity
- O(n) where n is the number of Promises
- All Promises execute concurrently, so the total time is determined by the slowest Promise
- This is much better than sequential execution which would be O(n × average_promise_time)
Space Complexity
- O(n) for the results array
- O(1) for the counter variable
- Total: O(n) where n is the number of Promises
Common Use Cases
- Parallel API calls: Fetch data from multiple endpoints simultaneously
- Database queries: Execute multiple queries concurrently
- File operations: Read multiple files at once
- Data validation: Validate multiple fields concurrently
- Resource loading: Load multiple resources (images, scripts) in parallel
Edge Cases to Consider
- Empty array: Should resolve with
[]immediately - All Promises reject: Should reject with the first rejection error
- Mixed Promises and values: Should handle both seamlessly
- Non-array input: Should handle gracefully (though not part of standard Promise.all spec)
- Sparse arrays: Should handle undefined values correctly
Production Considerations
In production, you might want to add:
- Input validation: Check if input is an array
- Type checking: Ensure all elements are thenable
- Performance monitoring: Track how long Promise.all takes
- Error context: Include which Promise failed in the error message
const promiseAll = (promises) => { if (!Array.isArray(promises)) { return Promise.reject(new TypeError('Expected an array')); } const results = []; let count = 0; const errors = []; return new Promise((resolve, reject) => { if (promises.length === 0) { resolve([]); return; } promises.forEach((promise, index) => { Promise.resolve(promise) .then((data) => { results[index] = data; count++; if (count === promises.length) { resolve(results); } }) .catch((error) => { reject(new Error(`Promise at index ${index} rejected: ${error}`)); }); }); }); };
Key Takeaways
- Concurrency: Promise.all executes all Promises simultaneously, not sequentially
- Order matters: Results maintain input order using array indexing
- Fail-fast: First rejection immediately rejects the entire operation
- Flexibility: Handles both Promises and regular values seamlessly
- Performance: Much faster than sequential execution for independent operations
Understanding Promise.all deeply helps you write more efficient asynchronous code and appreciate how JavaScript handles concurrency.