2795. Parallel Execution of Promises for Individual Results Retrieval
Problem Description
This problem asks you to implement a custom version of JavaScript's Promise.allSettled()
method without using the built-in function.
You're given an array called functions
, where each element is a function that returns a promise. Your task is to create a function that returns a single promise which resolves with an array of result objects.
Each function in the input array will return a promise that can either:
- Resolve successfully with some value
- Reject with some reason/error
For each promise, you need to create a result object with the following structure:
-
If the promise resolves successfully:
{ status: "fulfilled", value: resolvedValue }
-
If the promise rejects:
{ status: "rejected", reason: rejectionReason }
The key requirements are:
- Wait for all promises to settle (either resolve or reject) before returning the final result
- Never reject the main promise - even if some individual promises reject, the main promise should always resolve with the array of results
- Maintain the original order - the result at index
i
should correspond to the promise returned byfunctions[i]
- Include all results - both successful and failed promises should have their results included in the final array
For example, if you have 3 functions where the first resolves with value 15
, the second rejects with reason "error"
, and the third resolves with value 20
, the final result should be:
[ { status: "fulfilled", value: 15 }, { status: "rejected", reason: "error" }, { status: "fulfilled", value: 20 } ]
The solution uses a counter to track when all promises have settled and carefully preserves the order by using array indices when storing results.
Intuition
The core challenge here is that we need to wait for all promises to complete (whether they succeed or fail) before returning our result. Unlike Promise.all()
which fails fast on the first rejection, we need to collect results from every single promise.
The key insight is that we can transform both successful and failed promises into a consistent format. When a promise resolves or rejects, we can catch both outcomes and convert them into our desired object structure. This is achieved by chaining .then()
and .catch()
handlers to normalize the output.
Think of it this way: every promise has two possible outcomes, but we want to treat both outcomes uniformly. By using .then()
to handle success and .catch()
to handle failure, we can ensure that each promise ultimately produces an object with the same structure - just with different status values.
The trickiest part is knowing when all promises have settled. Since promises can complete in any order and at different times, we need a way to track progress. A simple counter works perfectly here - we increment it each time a promise settles, and when the counter equals the total number of functions, we know we're done.
Why do we use array indexing (res[i] = obj
) instead of pushing results? This is crucial for maintaining order. If we simply pushed results as they arrived, faster promises would appear first in our result array, breaking the correspondence with the original function array. By using the index from our loop, we ensure that each result lands in its correct position, regardless of completion order.
The outer Promise
wrapper is necessary because we're dealing with asynchronous operations. We can't simply return an array directly since we need to wait for all the async operations to complete. The resolve
function gives us a way to signal completion and return our collected results once everything is done.
Solution Approach
The implementation uses a promise wrapper pattern combined with manual tracking to simulate Promise.allSettled()
behavior.
Step 1: Create the outer Promise wrapper
return new Promise(resolve => { ... })
We wrap everything in a new Promise that will eventually resolve with our array of results. Note that we only need the resolve
parameter since this promise never rejects.
Step 2: Initialize tracking variables
const res: Obj[] = []; let count = 0;
res
: An array to store our result objects, pre-sized implicitly through index assignmentcount
: A counter to track how many promises have settled
Step 3: Iterate through functions with index preservation
for (let i in functions) { ... }
Using for...in
loop gives us access to indices, which is crucial for maintaining order in our results array.
Step 4: Execute each function and handle both outcomes
functions[i]()
.then(value => ({ status: 'fulfilled', value }))
.catch(reason => ({ status: 'rejected', reason }))
For each function:
- Call it to get the promise
- If it resolves,
.then()
transforms the value into{ status: 'fulfilled', value }
- If it rejects,
.catch()
transforms the reason into{ status: 'rejected', reason }
The key pattern here is that .catch()
after .then()
ensures we always get a resolved promise with our formatted object, regardless of the original promise's outcome.
Step 5: Store result and check completion
.then(obj => {
res[i] = obj;
if (++count === functions.length) {
resolve(res);
}
});
This final .then()
receives our formatted object (from either the success or failure path):
- Store the object at the correct index
i
to maintain order - Increment the counter using
++count
- Check if all promises have settled by comparing counter with array length
- If complete, resolve the outer promise with the results array
Why this approach works:
-
Order preservation: By using
res[i] = obj
instead ofres.push(obj)
, results always appear in the same order as the input functions, even if they complete out of order. -
Guaranteed settlement: The
.catch()
handler ensures that rejected promises don't break the flow - they're converted to result objects just like successful ones. -
Completion detection: The counter pattern is simple but effective - we don't need complex state management, just a number that tells us when we've processed everything.
-
Type safety: The TypeScript types (
FulfilledObj
,RejectedObj
,Obj
) ensure our result objects have the correct structure.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example with 3 functions to see how the solution works:
const functions = [
() => new Promise(resolve => setTimeout(() => resolve(5), 200)), // Resolves after 200ms
() => new Promise((_, reject) => setTimeout(() => reject("Error"), 100)), // Rejects after 100ms
() => new Promise(resolve => setTimeout(() => resolve(1), 150)) // Resolves after 150ms
]
Initial State:
res = []
(empty array to store results)count = 0
(no promises settled yet)- Outer promise created, waiting to resolve
Execution Timeline:
At t=0ms: All three functions are called
- Function 0: Promise pending (will resolve with 5 at 200ms)
- Function 1: Promise pending (will reject with "Error" at 100ms)
- Function 2: Promise pending (will resolve with 1 at 150ms)
At t=100ms: Function 1's promise rejects
- The
.catch()
handler transforms this into{ status: 'rejected', reason: "Error" }
res[1] = { status: 'rejected', reason: "Error" }
count = 1
(1 out of 3 settled)- Current
res
:[undefined, { status: 'rejected', reason: "Error" }, undefined]
At t=150ms: Function 2's promise resolves
- The
.then()
handler transforms this into{ status: 'fulfilled', value: 1 }
res[2] = { status: 'fulfilled', value: 1 }
count = 2
(2 out of 3 settled)- Current
res
:[undefined, { status: 'rejected', reason: "Error" }, { status: 'fulfilled', value: 1 }]
At t=200ms: Function 0's promise resolves
- The
.then()
handler transforms this into{ status: 'fulfilled', value: 5 }
res[0] = { status: 'fulfilled', value: 5 }
count = 3
(3 out of 3 settled)- Since
count === functions.length
, we callresolve(res)
Final Result:
[ { status: 'fulfilled', value: 5 }, { status: 'rejected', reason: "Error" }, { status: 'fulfilled', value: 1 } ]
Notice how:
- Despite completing in order 1→2→0, the results maintain the original array order 0→1→2
- The rejected promise (Function 1) didn't cause the overall operation to fail
- We waited for all promises to settle before resolving the outer promise
Solution Implementation
1from typing import List, Callable, Any, Union, TypedDict
2import asyncio
3
4# Type definition for a successfully resolved promise result
5class FulfilledObj(TypedDict):
6 status: str
7 value: Any
8
9# Type definition for a rejected promise result
10class RejectedObj(TypedDict):
11 status: str
12 reason: Any
13
14# Union type representing either fulfilled or rejected promise result
15Obj = Union[FulfilledObj, RejectedObj]
16
17async def promiseAllSettled(functions: List[Callable[[], Any]]) -> List[Obj]:
18 """
19 Executes all promise-returning functions and returns their settlement results
20 Similar to Promise.allSettled(), waits for all promises to settle (either fulfill or reject)
21
22 Args:
23 functions: List of functions that return coroutines/futures
24
25 Returns:
26 List of settlement results
27 """
28 # Array to store the settlement results in order
29 results = [None] * len(functions)
30
31 # Handle edge case: empty array
32 if len(functions) == 0:
33 return []
34
35 # Create a list to store all the tasks
36 tasks = []
37
38 # Iterate through each function using index to maintain order
39 for index in range(len(functions)):
40 # Define an async function to handle each promise
41 async def handle_promise(idx):
42 try:
43 # Execute the function and await its result
44 result = functions[idx]()
45
46 # If the result is a coroutine, await it
47 if asyncio.iscoroutine(result):
48 value = await result
49 else:
50 value = result
51
52 # Transform fulfilled promise to FulfilledObj
53 return idx, {'status': 'fulfilled', 'value': value}
54 except Exception as reason:
55 # Transform rejected promise to RejectedObj
56 return idx, {'status': 'rejected', 'reason': str(reason)}
57
58 # Add the task to the list
59 tasks.append(handle_promise(index))
60
61 # Wait for all tasks to complete
62 completed_results = await asyncio.gather(*tasks)
63
64 # Store results in the correct order
65 for idx, settlement_result in completed_results:
66 results[idx] = settlement_result
67
68 return results
69
1import java.util.ArrayList;
2import java.util.List;
3import java.util.concurrent.CompletableFuture;
4import java.util.concurrent.atomic.AtomicInteger;
5import java.util.function.Supplier;
6
7/**
8 * Base class for promise settlement results
9 */
10abstract class Obj {
11 protected String status;
12
13 public String getStatus() {
14 return status;
15 }
16}
17
18/**
19 * Type definition for a successfully resolved promise result
20 */
21class FulfilledObj extends Obj {
22 private String value;
23
24 public FulfilledObj(String value) {
25 this.status = "fulfilled";
26 this.value = value;
27 }
28
29 public String getValue() {
30 return value;
31 }
32}
33
34/**
35 * Type definition for a rejected promise result
36 */
37class RejectedObj extends Obj {
38 private String reason;
39
40 public RejectedObj(String reason) {
41 this.status = "rejected";
42 this.reason = reason;
43 }
44
45 public String getReason() {
46 return reason;
47 }
48}
49
50/**
51 * Promise settlement utility class
52 */
53class PromiseSettlement {
54
55 /**
56 * Executes all promise-returning functions and returns their settlement results
57 * Similar to Promise.allSettled(), waits for all promises to settle (either fulfill or reject)
58 *
59 * @param functions - List of suppliers that return CompletableFutures
60 * @return CompletableFuture that resolves with a list of settlement results
61 */
62 public static CompletableFuture<List<Obj>> promiseAllSettled(List<Supplier<CompletableFuture<String>>> functions) {
63 return CompletableFuture.supplyAsync(() -> {
64 // List to store the settlement results in order
65 List<Obj> results = new ArrayList<>();
66
67 // Initialize results list with null placeholders to maintain order
68 for (int i = 0; i < functions.size(); i++) {
69 results.add(null);
70 }
71
72 // Handle edge case: empty list
73 if (functions.isEmpty()) {
74 return results;
75 }
76
77 // Counter to track how many promises have settled
78 AtomicInteger settledCount = new AtomicInteger(0);
79
80 // List to store all CompletableFutures
81 List<CompletableFuture<Void>> futures = new ArrayList<>();
82
83 // Iterate through each function using index to maintain order
84 for (int index = 0; index < functions.size(); index++) {
85 final int currentIndex = index;
86
87 // Execute the function and handle both success and failure cases
88 CompletableFuture<Void> future = functions.get(index).get()
89 .handle((value, throwable) -> {
90 Obj settlementResult;
91
92 if (throwable == null) {
93 // Transform fulfilled promise to FulfilledObj
94 settlementResult = new FulfilledObj(value);
95 } else {
96 // Transform rejected promise to RejectedObj
97 settlementResult = new RejectedObj(throwable.getMessage());
98 }
99
100 // Store the result at the correct index
101 results.set(currentIndex, settlementResult);
102
103 // Increment counter
104 settledCount.incrementAndGet();
105
106 return null;
107 });
108
109 futures.add(future);
110 }
111
112 // Wait for all promises to settle
113 CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
114
115 // All promises settled, return the complete results list
116 return results;
117 });
118 }
119}
120
1#include <vector>
2#include <functional>
3#include <future>
4#include <variant>
5#include <string>
6#include <memory>
7
8// Type definition for a successfully resolved promise result
9struct FulfilledObj {
10 std::string status = "fulfilled";
11 std::string value;
12
13 FulfilledObj(const std::string& val) : value(val) {}
14};
15
16// Type definition for a rejected promise result
17struct RejectedObj {
18 std::string status = "rejected";
19 std::string reason;
20
21 RejectedObj(const std::string& err) : reason(err) {}
22};
23
24// Union type representing either fulfilled or rejected promise result
25using Obj = std::variant<FulfilledObj, RejectedObj>;
26
27/**
28 * Executes all promise-returning functions and returns their settlement results
29 * Similar to Promise.allSettled(), waits for all promises to settle (either fulfill or reject)
30 *
31 * @param functions - Vector of functions that return futures
32 * @returns Future that resolves with a vector of settlement results
33 */
34std::future<std::vector<Obj>> promiseAllSettled(
35 const std::vector<std::function<std::future<std::string>()>>& functions) {
36
37 return std::async(std::launch::async, [functions]() -> std::vector<Obj> {
38 // Vector to store the settlement results in order
39 std::vector<Obj> results(functions.size());
40
41 // Vector to store all the futures
42 std::vector<std::future<Obj>> futures;
43
44 // Handle edge case: empty vector
45 if (functions.empty()) {
46 return results;
47 }
48
49 // Iterate through each function using index to maintain order
50 for (size_t index = 0; index < functions.size(); index++) {
51 // Launch async task for each function
52 futures.push_back(std::async(std::launch::async, [&functions, index]() -> Obj {
53 try {
54 // Execute the function and wait for the result
55 std::future<std::string> fut = functions[index]();
56 std::string value = fut.get();
57
58 // Transform fulfilled promise to FulfilledObj
59 return FulfilledObj(value);
60 } catch (const std::exception& e) {
61 // Transform rejected promise to RejectedObj
62 return RejectedObj(std::string(e.what()));
63 } catch (...) {
64 // Handle non-exception errors
65 return RejectedObj("Unknown error");
66 }
67 }));
68 }
69
70 // Wait for all futures to complete and collect results
71 for (size_t index = 0; index < futures.size(); index++) {
72 // Store the result at the correct index
73 results[index] = futures[index].get();
74 }
75
76 // All promises settled, return the complete results vector
77 return results;
78 });
79}
80
1// Type definition for a successfully resolved promise result
2type FulfilledObj = {
3 status: 'fulfilled';
4 value: string;
5};
6
7// Type definition for a rejected promise result
8type RejectedObj = {
9 status: 'rejected';
10 reason: string;
11};
12
13// Union type representing either fulfilled or rejected promise result
14type Obj = FulfilledObj | RejectedObj;
15
16/**
17 * Executes all promise-returning functions and returns their settlement results
18 * Similar to Promise.allSettled(), waits for all promises to settle (either fulfill or reject)
19 *
20 * @param functions - Array of functions that return promises
21 * @returns Promise that resolves with an array of settlement results
22 */
23function promiseAllSettled(functions: Function[]): Promise<Obj[]> {
24 return new Promise<Obj[]>((resolve) => {
25 // Array to store the settlement results in order
26 const results: Obj[] = [];
27
28 // Counter to track how many promises have settled
29 let settledCount = 0;
30
31 // Handle edge case: empty array
32 if (functions.length === 0) {
33 resolve(results);
34 return;
35 }
36
37 // Iterate through each function using index to maintain order
38 for (let index = 0; index < functions.length; index++) {
39 // Execute the function and handle both success and failure cases
40 functions[index]()
41 .then((value: string) => {
42 // Transform fulfilled promise to FulfilledObj
43 return { status: 'fulfilled' as const, value };
44 })
45 .catch((reason: string) => {
46 // Transform rejected promise to RejectedObj
47 return { status: 'rejected' as const, reason };
48 })
49 .then((settlementResult: Obj) => {
50 // Store the result at the correct index
51 results[index] = settlementResult;
52
53 // Increment counter and check if all promises have settled
54 settledCount++;
55 if (settledCount === functions.length) {
56 // All promises settled, resolve with the complete results array
57 resolve(results);
58 }
59 });
60 }
61 });
62}
63
Time and Space Complexity
Time Complexity: O(n)
where n
is the number of functions in the input array.
The algorithm iterates through the array of functions once using a for-in loop, executing each function exactly once. Each function execution, promise resolution/rejection handling, and array assignment operation takes O(1)
time. Therefore, the overall time complexity is O(n)
.
Note that the actual wall-clock time depends on the slowest promise to settle (since promises may execute asynchronously), but from an algorithmic perspective, each function is only processed once.
Space Complexity: O(n)
where n
is the number of functions in the input array.
The space complexity breakdown:
- The
res
array storesn
result objects, requiringO(n)
space - The
count
variable usesO(1)
space - Each promise chain creates temporary objects for status and value/reason, but these are stored in the
res
array, already accounted for - The closure created by the Promise constructor maintains references to
res
andcount
, but this doesn't add to the asymptotic complexity
Therefore, the total space complexity is O(n)
.
Common Pitfalls
1. Race Condition with Result Storage
A critical pitfall occurs when developers try to use array.push()
or list.append()
instead of index-based assignment. This breaks the ordering guarantee:
Incorrect approach:
// JavaScript - WRONG!
functions[i]()
.then(value => {
res.push({ status: 'fulfilled', value }); // Order not preserved!
if (++count === functions.length) resolve(res);
})
Why it fails: Promises settle in unpredictable order. If promise #2 completes before promise #0, using push()
would place promise #2's result at index 0, breaking the expected order.
Solution: Always use index-based assignment:
res[i] = { status: 'fulfilled', value }; // Preserves original order
2. Forgetting to Handle Both Success and Failure Paths
Another common mistake is handling only the success case, causing the function to hang indefinitely when any promise rejects:
Incorrect approach:
// This will never resolve if any promise rejects!
functions[i]().then(value => {
res[i] = { status: 'fulfilled', value };
if (++count === functions.length) resolve(res);
});
// Missing .catch() - rejected promises won't increment counter
Solution: Chain .catch()
to handle rejections and ensure the counter always increments:
functions[i]()
.then(value => ({ status: 'fulfilled', value }))
.catch(reason => ({ status: 'rejected', reason }))
.then(obj => {
res[i] = obj;
if (++count === functions.length) resolve(res);
});
3. Using Array Length Instead of Counter
Attempting to check completion by examining the results array can fail due to sparse arrays:
Incorrect approach:
// WRONG - sparse arrays don't report accurate length
res[i] = obj;
if (res.filter(x => x !== undefined).length === functions.length) {
resolve(res);
}
Why it fails: JavaScript arrays with gaps (e.g., [undefined, obj, undefined]
) may not accurately reflect completion status, especially if promises complete out of order.
Solution: Use a dedicated counter variable that increments atomically:
if (++count === functions.length) { resolve(res); }
4. Python-Specific: Closure Variable Capture
In the Python implementation, a subtle bug occurs with variable capture in nested functions:
Incorrect approach:
for index in range(len(functions)):
async def handle_promise(): # No parameter!
try:
result = functions[index]() # 'index' captured from outer scope
# ... rest of code
Why it fails: All nested functions capture the same index
variable, which will have the value of the last iteration when the functions execute.
Solution: Pass the index as a parameter to create a new scope:
async def handle_promise(idx): # Accept index as parameter
try:
result = functions[idx]() # Use the parameter, not outer variable
5. Not Pre-sizing the Results Array
Creating an empty array and trying to assign to arbitrary indices can cause issues:
Problematic approach:
const res = []; // Empty array // Later... res[5] = obj; // Creates sparse array: [empty × 5, obj]
Better approach for Python:
results = [None] * len(functions) # Pre-size the array
For JavaScript: The sparse array approach works but be aware of the implications when iterating or checking length.
What is an advantages of top-down dynamic programming vs bottom-up dynamic programming?
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!