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.

LeetCode Problem 2721

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:

  1. Starting every asynchronous function immediately.
  2. Tracking how many promises have resolved.
  3. Storing results at the correct index to preserve ordering.
  4. Resolving the returned promise only when every task finishes.
  5. Rejecting immediately if any promise fails.

To implement this, we maintain:

  • A results array of size n
  • A counter for completed promises
  • A wrapper Promise that 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

  1. 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)
  1. 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.