LeetCode 2721 - Execute Asynchronous Functions in Parallel
This problem asks us to implement a custom version of Promise.all, but without using the built in Promise.all API. We are given an array called functions, where every element is an asynchronous function. Each function takes no arguments and returns a promise when executed.
Difficulty: 🟡 Medium
Topics: —
Solution
Problem Understanding
This problem asks us to implement a custom version of Promise.all, but without using the built in Promise.all API.
We are given an array called functions, where every element is an asynchronous function. Each function takes no arguments and returns a promise when executed. The key requirement is that all functions must start executing immediately and in parallel, not one after another.
The function we implement should return a single promise, called promise, with the following behavior:
If every asynchronous function resolves successfully, the returned promise should resolve with an array of results. The results must appear in the same order as the original functions array, regardless of which promises finish first.
If any promise rejects, the returned promise should immediately reject with the reason from the first rejection. We do not wait for the remaining promises to finish.
To understand the input more clearly, consider that functions contains executable asynchronous operations. For example:
[
() => Promise.resolve(1),
() => Promise.resolve(2)
]
Each function must be called to start execution. Simply iterating over the array is not enough, because the functions themselves are only definitions until invoked.
The output is a promise that behaves like an aggregate controller. It waits for all successful completions or fails immediately when the first error occurs.
The constraints are very small:
1 <= functions.length <= 10
This tells us that performance is not a major issue. Even an inefficient implementation would likely pass. However, the goal of the problem is to correctly handle asynchronous coordination and promise resolution mechanics.
There are several important edge cases to keep in mind.
First, promises may resolve in a different order than they were started. We must preserve the original ordering in the final result array.
Second, one promise may reject before others resolve. The returned promise must reject immediately with that error.
Third, a single function may exist in the array. The solution should still behave correctly and resolve after that single promise completes.
Finally, multiple promises might finish at nearly the same time. A naive implementation could accidentally resolve the final promise more than once or produce incorrect ordering.
Approaches
Brute Force Approach
A straightforward approach is to execute each function sequentially using await.
We could iterate through functions, call each function, wait for it to resolve, append the result to an array, then move to the next function.
This produces the correct ordering and naturally handles rejections because any thrown error would stop execution. However, it violates the problem requirement that all asynchronous operations must execute in parallel.
For example, if three promises each take 100ms, a sequential approach would take about 300ms instead of 100ms.
Although this approach is simple and correct in terms of output, it is not suitable because it sacrifices concurrency.
Optimal Approach
The key observation is that all functions should start immediately, but we still need to know when all of them finish.
We can achieve this by:
- Starting every asynchronous function immediately.
- Tracking how many promises have resolved.
- Storing results at the correct index to preserve ordering.
- Resolving the returned promise only when every task finishes.
- Rejecting immediately if any promise fails.
To implement this, we maintain:
- A
resultsarray of sizen - A counter for completed promises
- A wrapper
Promisethat resolves or rejects manually
Each function is executed right away. When a promise resolves, we store its result at the corresponding index and increment the completion counter. Once the counter equals the number of functions, we resolve the final promise.
If any promise rejects, we immediately reject the wrapper promise.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n) | O(n) | Executes sequentially, violates parallel execution requirement |
| Optimal | O(n) | O(n) | Executes all promises in parallel and preserves ordering |
Algorithm Walkthrough
- Create and return a new promise.
We need manual control over when the final promise resolves or rejects, so we wrap everything inside a new Promise((resolve, reject) => {}).
2. Initialize a results array.
Create an array of size n to store resolved values. This ensures we preserve the original ordering of functions, even if promises finish out of order.
3. Initialize a completion counter.
We track how many promises have resolved successfully. This allows us to know when all asynchronous work has finished. 4. Iterate through every function.
For each function at index i, immediately invoke it so execution begins in parallel.
5. Attach resolution and rejection handlers.
When a promise resolves:
- Store the value at
results[i] - Increment the completion counter
- If all promises are completed, call
resolve(results)
When a promise rejects:
- Immediately call
reject(error)
- Return the wrapped promise.
The caller receives a promise that behaves exactly like Promise.all, except we implemented it manually.
Why it works
The algorithm works because every asynchronous function is started immediately, guaranteeing parallel execution. The results array preserves ordering by storing values at fixed indices, independent of completion timing. The completion counter ensures the final promise resolves only after every promise succeeds. If any promise fails, rejection happens immediately, matching the required behavior.
Python Solution
Since this is a JavaScript promise problem, LeetCode expects a JavaScript implementation. Python does not have an equivalent official submission stub for this problem. Below is the correct LeetCode-submittable JavaScript solution.
# This problem is JavaScript-specific on LeetCode.
# Python submission is not applicable.
The core idea of the implementation is to manually coordinate asynchronous execution. We immediately invoke every function to begin execution in parallel. A results array stores resolved values by index so ordering remains correct. A counter tracks completed promises, and once every promise resolves successfully, the final promise resolves with the collected results.
Rejections are handled immediately by forwarding the error to reject, which mirrors the behavior of Promise.all.
JavaScript Solution
/**
* @param {Array<Function>} functions
* @return {Promise<any>}
*/
var promiseAll = function(functions) {
return new Promise((resolve, reject) => {
const n = functions.length;
const results = new Array(n);
let completed = 0;
for (let i = 0; i < n; i++) {
functions[i]()
})
});
};
The implementation begins by creating a wrapper promise. We allocate a results array to preserve ordering and a completed counter to track progress.
Each function is immediately invoked inside the loop, ensuring parallel execution. The .then() handler stores the resolved value at the correct index and increments the counter. Once all promises resolve, we return the full results array.
The .catch() handler immediately rejects the wrapper promise, ensuring the first rejection terminates the process as required.
Go Solution
This problem is JavaScript-specific because it depends on native Promise behavior. Go does not have a direct LeetCode submission version for this problem.
// This problem is JavaScript-specific on LeetCode.
// Go submission is not applicable.
package main
The primary language specific difference is that JavaScript provides built in promise primitives and asynchronous execution semantics. Go uses goroutines and channels instead of promises, so a direct one to one implementation does not exist in the LeetCode environment.
Worked Examples
Example 1
Input
[
() => new Promise(resolve => setTimeout(() => resolve(5), 200))
]
Only one function exists.
| Step | Action | Results | Completed |
|---|---|---|---|
| 1 | Execute function | [empty] |
0 |
| 2 | Promise resolves with 5 |
[5] |
1 |
| 3 | completed === n |
[5] |
1 |
Final output:
[5]
The promise resolves after 200ms.
Example 2
Input
[
() => new Promise(resolve => setTimeout(() => resolve(1), 200)),
() => new Promise((resolve, reject) =>
setTimeout(() => reject("Error"), 100))
]
Both functions start immediately.
| Time | Event | Results | State |
|---|---|---|---|
| 0ms | Start both promises | [empty, empty] |
Running |
| 100ms | Second promise rejects | [empty, empty] |
Reject |
| 200ms | First promise resolves | Ignored | Already rejected |
Final output:
"Error"
The returned promise rejects immediately at 100ms.
Example 3
Input
[
() => new Promise(resolve => setTimeout(() => resolve(4), 50)),
() => new Promise(resolve => setTimeout(() => resolve(10), 150)),
() => new Promise(resolve => setTimeout(() => resolve(16), 100))
]
All functions begin immediately.
| Time | Event | Results | Completed |
|---|---|---|---|
| 0ms | Start all promises | [empty, empty, empty] |
0 |
| 50ms | First resolves with 4 |
[4, empty, empty] |
1 |
| 100ms | Third resolves with 16 |
[4, empty, 16] |
2 |
| 150ms | Second resolves with 10 |
[4, 10, 16] |
3 |
Final output:
[4, 10, 16]
Notice that completion order differs from result order. The result still matches the original function order.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | Each function is invoked once and each promise is processed once |
| Space | O(n) | We store results for all promises |
The algorithm performs a single pass through the functions array and stores one result per function. Even though execution happens asynchronously, the total amount of work scales linearly with the number of promises.
Test Cases
# This problem is JavaScript-specific.
# The following are conceptual test scenarios.
# Single promise
# Expected: [5]
# Mixed resolve and reject
# Expected: reject("Error")
# Multiple parallel resolutions
# Expected: [4, 10, 16]
# Different completion order
# Expected ordering preserved
# Immediate resolution
# Expected: resolves instantly
# Immediate rejection
# Expected: rejects instantly
| Test | Why |
|---|---|
| Single function | Verifies smallest valid input |
| One rejection | Ensures immediate rejection behavior |
| Multiple resolutions | Confirms all promises execute in parallel |
| Out of order completion | Validates result ordering |
| Immediate resolve | Checks zero delay behavior |
| Immediate reject | Verifies fast failure handling |
Edge Cases
One important edge case is when promises resolve in a different order than they were started. A naive implementation might append results as promises complete, producing incorrect ordering. Our implementation avoids this by storing results at their original indices.
Another important case occurs when one promise rejects before others finish. The returned promise must reject immediately instead of waiting for remaining tasks. Since we call reject(error) as soon as a rejection happens, the implementation behaves correctly.
A third edge case is having only a single asynchronous function in the input array. Some implementations accidentally assume multiple promises exist or mishandle completion counting. Our counter based approach naturally handles this case because the promise resolves as soon as the single function completes.