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
ishould 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 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 691import 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} 1201#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} 801// 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} 63Solution 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
ito 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] = objinstead 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
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
resarray storesnresult objects, requiringO(n)space - The
countvariable usesO(1)space - Each promise chain creates temporary objects for status and value/reason, but these are stored in the
resarray, already accounted for - The closure created by the Promise constructor maintains references to
resandcount, 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.
In a binary min heap, the minimum element can be found in:
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 If you prefer videos here's a video that explains recursion in a fun and easy way 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 him
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!