Facebook Pixel

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:

  1. Wait for all promises to settle (either resolve or reject) before returning the final result
  2. Never reject the main promise - even if some individual promises reject, the main promise should always resolve with the array of results
  3. Maintain the original order - the result at index i should correspond to the promise returned by functions[i]
  4. 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 assignment
  • count: 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:

  1. Order preservation: By using res[i] = obj instead of res.push(obj), results always appear in the same order as the input functions, even if they complete out of order.

  2. Guaranteed settlement: The .catch() handler ensures that rejected promises don't break the flow - they're converted to result objects just like successful ones.

  3. 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.

  4. 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 Evaluator

Example 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 call resolve(res)

Final Result:

[
    { status: 'fulfilled', value: 5 },
    { status: 'rejected', reason: "Error" },
    { status: 'fulfilled', value: 1 }
]

Notice how:

  1. Despite completing in order 1→2→0, the results maintain the original array order 0→1→2
  2. The rejected promise (Function 1) didn't cause the overall operation to fail
  3. 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 stores n result objects, requiring O(n) space
  • The count variable uses O(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 and count, 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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What is an advantages of top-down dynamic programming vs bottom-up dynamic programming?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More