2721. Execute Asynchronous Functions in Parallel
Problem Description
This problem asks you to implement a function that mimics the behavior of Promise.all()
without using the built-in method. You need to create a function called promiseAll
that takes an array of asynchronous functions as input.
Each function in the input array:
- Takes no arguments
- Returns a promise when called
Your promiseAll
function should return a new promise that behaves as follows:
Success Case:
- The returned promise should resolve when ALL promises from the input functions have resolved successfully
- The resolved value should be an array containing all the resolved values
- The order of values in the result array must match the order of functions in the input array
- All functions should execute in parallel (not sequentially)
Failure Case:
- If ANY promise rejects, the returned promise should immediately reject
- The rejection reason should be the same as the first promise that rejected
- Once a rejection occurs, it doesn't matter if other promises are still pending
For example, if you have three async functions that resolve to values [1, 2, 3]
, your function should return a promise that resolves to [1, 2, 3]
. But if the second function rejects with error "failed", your promise should reject with "failed" regardless of whether the other functions succeed.
The solution uses a counter to track completed promises and an array to store results at their correct indices. When the counter reaches the total number of functions, all promises have resolved and the result array is returned. If any promise rejects, the entire operation immediately rejects with that error.
Intuition
To understand how to solve this problem, let's think about what Promise.all
needs to accomplish. We have multiple asynchronous operations running in parallel, and we need to know when they're all done while preserving their order.
The key insight is that we need to track two things:
- How many promises have completed - to know when we're done
- The results in their original order - even though promises may resolve in any order
Since promises can resolve at different times, we can't just push results into an array as they come in - that would lose the original ordering. Instead, we need to place each result at its specific index position.
Think of it like waiting for multiple friends to arrive at a party. You have a list of who's coming, and you need to know when everyone has arrived. Each friend might arrive at different times, but you want to remember them in the order they were on your original guest list, not the order they showed up.
This leads us to use:
- A counter (
cnt
) to track how many promises have resolved - A pre-allocated array (
ans
) where we can place results at their correct indices - A loop with closures to capture each index
i
, ensuring each promise's result goes to the right spot
For error handling, we adopt a "fail-fast" approach - as soon as any promise rejects, we immediately reject the entire operation. This is like a chain being only as strong as its weakest link. We don't need to wait for other promises because the overall operation has already failed.
The wrapper new Promise((resolve, reject) => {...})
gives us control over when to resolve (when counter equals array length) or reject (on first error) the overall promise.
Solution Approach
Let's walk through the implementation step by step:
1. Create a wrapper promise:
return new Promise<T[]>((resolve, reject) => {
// Implementation here
});
We wrap everything in a new promise that we can control - deciding when to resolve with all results or reject with an error.
2. Initialize tracking variables:
let cnt = 0;
const ans = new Array(functions.length);
cnt
tracks how many promises have successfully resolvedans
is a pre-allocated array with the same length as the input functions array to store results at their correct positions
3. Execute all functions in parallel:
for (let i = 0; i < functions.length; ++i) {
const f = functions[i];
f()
.then(res => {
// Handle success
})
.catch(err => {
// Handle failure
});
}
The loop immediately invokes all functions without waiting for any to complete. Each function call returns a promise that we attach handlers to.
4. Handle successful resolution:
.then(res => {
ans[i] = res;
cnt++;
if (cnt === functions.length) {
resolve(ans);
}
})
When a promise resolves:
- Store the result at index
i
in theans
array (preserving order) - Increment the counter
- Check if all promises are done (
cnt === functions.length
) - If all are complete, resolve the main promise with the complete results array
5. Handle rejection:
.catch(err => {
reject(err);
})
If any promise rejects, immediately reject the main promise with the same error. This implements the "fail-fast" behavior where the first rejection causes the entire operation to fail.
Key implementation details:
- The closure in the loop captures the value of
i
for each iteration, ensuring each result goes to the correct index - All functions are invoked immediately in the loop, achieving parallel execution
- The counter approach avoids needing to check the array for completion on every resolution
- No additional error tracking is needed since we reject on the first error
This implementation efficiently handles both the happy path (all succeed) and error cases (any failure) while maintaining the correct order of results.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example with 3 async functions to see how the solution works:
Input functions:
fn1
: resolves with value10
after 100msfn2
: resolves with value20
after 50msfn3
: resolves with value30
after 150ms
Step-by-step execution:
-
Initialization (t=0ms):
- Create wrapper promise
- Set
cnt = 0
- Create
ans = [undefined, undefined, undefined]
(array of length 3)
-
Start all functions (t=0ms):
- Loop executes immediately:
i=0
: Callfn1()
, attach.then()
and.catch()
handlersi=1
: Callfn2()
, attach.then()
and.catch()
handlersi=2
: Callfn3()
, attach.then()
and.catch()
handlers
- All three functions are now running in parallel
- Loop executes immediately:
-
First resolution (t=50ms):
fn2
resolves with20
(it's the fastest)- Its
.then()
handler executes withi=1
:ans[1] = 20
→ans = [undefined, 20, undefined]
cnt = 1
- Check:
cnt (1) !== functions.length (3)
, so continue waiting
-
Second resolution (t=100ms):
fn1
resolves with10
- Its
.then()
handler executes withi=0
:ans[0] = 10
→ans = [10, 20, undefined]
cnt = 2
- Check:
cnt (2) !== functions.length (3)
, so continue waiting
-
Final resolution (t=150ms):
fn3
resolves with30
- Its
.then()
handler executes withi=2
:ans[2] = 30
→ans = [10, 20, 30]
cnt = 3
- Check:
cnt (3) === functions.length (3)
✓ - Call
resolve(ans)
with[10, 20, 30]
Result: The wrapper promise resolves with [10, 20, 30]
- values are in the original order despite fn2
finishing first.
Alternative scenario with rejection:
If fn2
had rejected with error "Network error"
at t=50ms:
- Its
.catch()
handler would execute immediately - Call
reject("Network error")
- The wrapper promise rejects with
"Network error"
fn1
andfn3
continue running but their results are ignored
This demonstrates how the solution maintains order through indexed assignment and implements fail-fast behavior on errors.
Solution Implementation
1import asyncio
2from typing import List, Callable, TypeVar, Awaitable
3
4T = TypeVar('T')
5
6async def promiseAll(functions: List[Callable[[], Awaitable[T]]]) -> List[T]:
7 """
8 Executes an array of coroutine-returning functions in parallel and returns
9 a list of all resolved values in order.
10 If any coroutine raises an exception, the entire operation fails immediately.
11
12 Args:
13 functions: List of functions that return coroutines (async functions)
14
15 Returns:
16 List of resolved values from all coroutines
17 """
18 # Create a list to store all coroutine objects
19 coroutines = []
20
21 # Execute each coroutine-returning function to get the coroutine objects
22 for function in functions:
23 # Call the function to get the coroutine and add it to the list
24 coroutines.append(function())
25
26 # Use asyncio.gather to run all coroutines concurrently
27 # If any coroutine raises an exception, gather will raise it immediately
28 # The results are returned in the same order as the input
29 results = await asyncio.gather(*coroutines)
30
31 return list(results)
32
33
34# Example usage:
35# async def example():
36# result = await promiseAll([lambda: asyncio.coroutine(lambda: 42)()])
37# print(result) # [42]
38
39# Alternative implementation that more closely mirrors the JavaScript behavior:
40async def promiseAll_alternative(functions: List[Callable[[], Awaitable[T]]]) -> List[T]:
41 """
42 Alternative implementation that manually tracks completion similar to the JS version.
43
44 Args:
45 functions: List of functions that return coroutines
46
47 Returns:
48 List of resolved values from all coroutines
49 """
50 # Initialize result list with the same length as input
51 results = [None] * len(functions)
52
53 # Create tasks for all coroutines
54 tasks = []
55 for index, promise_function in enumerate(functions):
56 # Create a task for each coroutine function
57 task = asyncio.create_task(promise_function())
58 tasks.append((index, task))
59
60 # Wait for all tasks to complete
61 for index, task in tasks:
62 try:
63 # Wait for the task and store result at corresponding index
64 result = await task
65 results[index] = result
66 except Exception as error:
67 # If any task fails, cancel all remaining tasks
68 for _, remaining_task in tasks:
69 if not remaining_task.done():
70 remaining_task.cancel()
71 # Re-raise the exception
72 raise error
73
74 return results
75
1import java.util.concurrent.CompletableFuture;
2import java.util.concurrent.CompletionException;
3import java.util.function.Supplier;
4import java.util.List;
5import java.util.ArrayList;
6import java.util.concurrent.atomic.AtomicInteger;
7
8/**
9 * Utility class for handling multiple asynchronous operations in parallel.
10 */
11public class PromiseUtils {
12
13 /**
14 * Executes an array of CompletableFuture-returning functions in parallel and returns
15 * a CompletableFuture that completes with a list of all resolved values in order.
16 * If any CompletableFuture fails, the entire operation fails immediately.
17 *
18 * @param <T> The type of the result values
19 * @param functions - List of suppliers that return CompletableFutures
20 * @return CompletableFuture that completes with a list of resolved values
21 */
22 public static <T> CompletableFuture<List<T>> promiseAll(List<Supplier<CompletableFuture<T>>> functions) {
23 // Create the result CompletableFuture that will be returned
24 CompletableFuture<List<T>> resultFuture = new CompletableFuture<>();
25
26 // Handle edge case of empty input
27 if (functions.isEmpty()) {
28 resultFuture.complete(new ArrayList<>());
29 return resultFuture;
30 }
31
32 // Track the number of completed futures
33 AtomicInteger completedCount = new AtomicInteger(0);
34
35 // Initialize result list with the same size as input
36 List<T> results = new ArrayList<>();
37 for (int i = 0; i < functions.size(); i++) {
38 results.add(null);
39 }
40
41 // Execute each CompletableFuture-returning function
42 for (int index = 0; index < functions.size(); index++) {
43 final int currentIndex = index;
44 Supplier<CompletableFuture<T>> futureSupplier = functions.get(index);
45
46 // Execute the supplier and handle its CompletableFuture
47 futureSupplier.get()
48 .whenComplete((result, throwable) -> {
49 if (throwable != null) {
50 // If any future fails, immediately fail the main future
51 resultFuture.completeExceptionally(throwable);
52 } else {
53 // Store the result at the corresponding index
54 synchronized (results) {
55 results.set(currentIndex, result);
56 }
57
58 // Increment completed count and check if all are done
59 if (completedCount.incrementAndGet() == functions.size()) {
60 // If all futures have completed, complete the main future
61 resultFuture.complete(results);
62 }
63 }
64 });
65 }
66
67 return resultFuture;
68 }
69
70 /**
71 * Example usage:
72 * List<Supplier<CompletableFuture<Integer>>> suppliers = List.of(
73 * () -> CompletableFuture.completedFuture(42)
74 * );
75 * CompletableFuture<List<Integer>> future = promiseAll(suppliers);
76 * future.thenAccept(System.out::println); // [42]
77 */
78}
79
1#include <vector>
2#include <future>
3#include <functional>
4#include <memory>
5
6/**
7 * Executes an array of promise-returning functions in parallel and returns
8 * a future that completes with a vector of all resolved values in order.
9 * If any promise fails, the entire operation fails immediately.
10 *
11 * @param functions - Vector of functions that return futures
12 * @returns Future that resolves to a vector of resolved values
13 */
14template<typename T>
15std::future<std::vector<T>> promiseAll(const std::vector<std::function<std::future<T>()>>& functions) {
16 // Create a promise to control the final result
17 auto finalPromise = std::make_shared<std::promise<std::vector<T>>>();
18
19 // Get the future from the promise
20 std::future<std::vector<T>> finalFuture = finalPromise->get_future();
21
22 // Handle empty input case
23 if (functions.empty()) {
24 finalPromise->set_value(std::vector<T>());
25 return finalFuture;
26 }
27
28 // Shared state for tracking completion
29 struct SharedState {
30 std::mutex mutex;
31 std::vector<T> results;
32 size_t resolvedCount = 0;
33 bool hasRejected = false;
34 };
35
36 auto sharedState = std::make_shared<SharedState>();
37 sharedState->results.resize(functions.size());
38
39 // Execute each promise-returning function
40 for (size_t index = 0; index < functions.size(); ++index) {
41 // Launch async task for each function
42 std::thread([index, functions, sharedState, finalPromise]() {
43 try {
44 // Execute the function and get its future
45 std::future<T> future = functions[index]();
46
47 // Wait for the result
48 T result = future.get();
49
50 // Store the result thread-safely
51 {
52 std::lock_guard<std::mutex> lock(sharedState->mutex);
53
54 // Check if already rejected
55 if (sharedState->hasRejected) {
56 return;
57 }
58
59 // Store the result at the corresponding index
60 sharedState->results[index] = result;
61 sharedState->resolvedCount++;
62
63 // If all promises have resolved, resolve the main promise
64 if (sharedState->resolvedCount == functions.size()) {
65 finalPromise->set_value(sharedState->results);
66 }
67 }
68 }
69 catch (const std::exception& e) {
70 // Handle any exception from the future
71 std::lock_guard<std::mutex> lock(sharedState->mutex);
72
73 // If not already rejected, reject the main promise
74 if (!sharedState->hasRejected) {
75 sharedState->hasRejected = true;
76 finalPromise->set_exception(std::current_exception());
77 }
78 }
79 }).detach();
80 }
81
82 return finalFuture;
83}
84
85/**
86 * Example usage:
87 * auto promise = promiseAll<int>({
88 * []() { return std::async(std::launch::async, []() { return 42; }); }
89 * });
90 * std::vector<int> results = promise.get(); // [42]
91 */
92
1/**
2 * Executes an array of promise-returning functions in parallel and returns
3 * a promise that resolves with an array of all resolved values in order.
4 * If any promise rejects, the entire operation rejects immediately.
5 *
6 * @param functions - Array of functions that return promises
7 * @returns Promise that resolves to an array of resolved values
8 */
9async function promiseAll<T>(functions: (() => Promise<T>)[]): Promise<T[]> {
10 return new Promise<T[]>((resolve, reject) => {
11 // Track the number of resolved promises
12 let resolvedCount = 0;
13
14 // Initialize result array with the same length as input
15 const results = new Array(functions.length);
16
17 // Execute each promise-returning function
18 for (let index = 0; index < functions.length; index++) {
19 const promiseFunction = functions[index];
20
21 // Execute the function and handle its promise
22 promiseFunction()
23 .then(result => {
24 // Store the result at the corresponding index
25 results[index] = result;
26 resolvedCount++;
27
28 // If all promises have resolved, resolve the main promise
29 if (resolvedCount === functions.length) {
30 resolve(results);
31 }
32 })
33 .catch(error => {
34 // If any promise rejects, immediately reject the main promise
35 reject(error);
36 });
37 }
38 });
39}
40
41/**
42 * Example usage:
43 * const promise = promiseAll([() => new Promise(res => res(42))])
44 * promise.then(console.log); // [42]
45 */
46
Time and Space Complexity
Time Complexity: O(n)
where n
is the number of functions in the input array.
The code iterates through the array of functions once with a for loop, and for each function, it invokes it and attaches .then()
and .catch()
handlers. The iteration itself takes O(n)
time. Each promise is executed asynchronously and in parallel, so the actual execution time depends on the slowest promise to resolve, but the setup and handling of promises is O(n)
.
Space Complexity: O(n)
where n
is the number of functions in the input array.
The space complexity includes:
- The
ans
array which stores the results of all promises:O(n)
- The closure variables (
cnt
,resolve
,reject
) which are constant:O(1)
- Each promise's
.then()
and.catch()
handlers create closures that capture the indexi
and reference toans
,cnt
,resolve
, andreject
:O(n)
total for all handlers
Therefore, the overall space complexity is O(n)
.
Common Pitfalls
1. Using the Loop Variable Directly in Callbacks (JavaScript/TypeScript)
Pitfall: A classic mistake is capturing the loop variable incorrectly in the promise handlers:
// WRONG - This will not preserve the correct order
for (var i = 0; i < functions.length; i++) {
functions[i]()
.then(res => {
ans[i] = res; // Bug: 'i' will be functions.length for all callbacks
cnt++;
if (cnt === functions.length) {
resolve(ans);
}
})
.catch(reject);
}
Why it fails: When using var
, the variable i
is function-scoped, not block-scoped. By the time the promises resolve, the loop has completed and i
equals functions.length
for all callbacks, causing all results to be stored at an invalid index.
Solution: Use let
for block scoping or create a closure:
// Solution 1: Use 'let' for block scoping
for (let i = 0; i < functions.length; i++) {
// 'i' is now block-scoped and captured correctly
}
// Solution 2: Create an IIFE closure
for (var i = 0; i < functions.length; i++) {
(function(index) {
functions[index]()
.then(res => {
ans[index] = res;
// ...
})
})(i);
}
2. Not Handling Empty Arrays
Pitfall: Forgetting to handle the edge case when the input array is empty:
// This works but could be more explicit if (cnt === functions.length) { // When length is 0, this never triggers resolve(ans); }
Solution: Add explicit handling for empty arrays:
if (functions.length === 0) { resolve([]); return; }
3. Sequential Execution Instead of Parallel (Python)
Pitfall: Accidentally awaiting each coroutine in sequence:
# WRONG - This executes sequentially results = [] for func in functions: result = await func() # Waits for each one to complete before starting the next results.append(result)
Solution: Create all coroutines first, then await them together:
# Correct - Executes in parallel coroutines = [func() for func in functions] results = await asyncio.gather(*coroutines)
4. Not Preserving Order When Using Manual Tracking
Pitfall: Appending results as they arrive instead of placing them at their original index:
// WRONG - Order is not preserved
functions[i]()
.then(res => {
ans.push(res); // Results will be in completion order, not original order
cnt++;
if (cnt === functions.length) {
resolve(ans);
}
})
Solution: Always use the index to place results:
// Correct - Preserves original order ans[i] = res; // Place at specific index
5. Memory Leaks with Unhandled Rejections
Pitfall: When implementing manual cancellation in Python, forgetting to properly clean up tasks:
# Potential issue - tasks might not be properly cancelled for index, task in tasks: try: result = await task except Exception as error: # Other tasks continue running in the background raise error
Solution: Ensure all pending tasks are cancelled on failure:
except Exception as error: # Cancel all remaining tasks for _, remaining_task in tasks: if not remaining_task.done(): remaining_task.cancel() raise error
Which algorithm should you use to find a node that is close to the root of the tree?
Recommended Readings
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!