LeetCode 2795 - Parallel Execution of Promises for Individual Results Retrieval

This problem asks us to implement a function similar to JavaScript's Promise.allSettled(), but without actually using the built-in method. We are given an array of functions functions, where each function, when invoked, returns a promise.

LeetCode Problem 2795

Difficulty: 🟡 Medium
Topics:

Solution

Problem Understanding

This problem asks us to implement a function similar to JavaScript's Promise.allSettled(), but without actually using the built-in method. We are given an array of functions functions, where each function, when invoked, returns a promise. These promises can either resolve successfully with a value or reject with an error. Our task is to return a single promise that resolves to an array of result objects. Each result object corresponds to the promise returned by a function in the input array, preserving the original order.

Each result object has the form:

  • If the promise resolves: { status: "fulfilled", value: resolvedValue }
  • If the promise rejects: { status: "rejected", reason: errorMessage }

The constraints are small (1 <= functions.length <= 10), so performance is not a major concern, but we must handle both resolution and rejection properly and preserve the order.

Important edge cases include: a single function, functions that reject, multiple functions with varying resolve/reject times, and functions that resolve instantly or asynchronously. We must ensure that the result array corresponds exactly in order to the input functions array.

Approaches

Brute Force

A naive approach would be to sequentially call each function and wait for its promise to resolve or reject before moving on to the next function. This would involve using .then() and .catch() for each promise in a chain. While this produces correct results, it is inefficient because promises are executed sequentially rather than concurrently. Since the constraints allow up to 10 functions, this is feasible but unnecessarily slow for asynchronous operations.

Optimal Approach

The optimal solution is to execute all functions concurrently. We can map over the functions array, invoking each function to get a promise, then wrap each promise with .then() and .catch() to transform its result into the expected {status, value/reason} object. By using Promise.resolve().then(...) on each function’s promise, we ensure both resolved and rejected cases are handled uniformly. Finally, we use Promise.all() to wait for all wrapped promises to settle. This approach preserves the original order because Promise.all() maintains the order of the input array.

Approach Time Complexity Space Complexity Notes
Brute Force O(n * t_max) O(n) Sequentially executes promises, slowest for concurrent tasks
Optimal O(t_max) O(n) Executes all promises concurrently, wraps results to handle both fulfillment and rejection

Algorithm Walkthrough

  1. Initialize an array wrappedPromises by mapping over the input functions array. For each function fn, invoke it to get a promise.
  2. Wrap each promise with .then() to handle resolution and .catch() to handle rejection. Convert both into objects of the form {status: "fulfilled", value} or {status: "rejected", reason}.
  3. Use Promise.all() on the array of wrapped promises to create a single promise that resolves when all wrapped promises complete. The order is preserved because Promise.all() respects the input array order.
  4. Return this final promise.

Why it works: By wrapping each promise, we guarantee that even rejected promises are transformed into objects rather than causing the aggregate promise to reject. Using Promise.all() ensures that all promises are executed concurrently, and the resulting array maintains the original order of functions.

Python Solution

from typing import List, Dict, Any, Callable
import asyncio

async def promiseAllSettled(functions: List[Callable[[], Any]]) -> List[Dict[str, Any]]:
    async def wrap(fn):
        try:
            result = await fn()
            return {"status": "fulfilled", "value": result}
        except Exception as e:
            return {"status": "rejected", "reason": str(e)}
    
    return await asyncio.gather(*(wrap(fn) for fn in functions))

In this Python solution, we use asyncio to mimic the asynchronous behavior of JavaScript promises. The wrap coroutine handles each function, awaiting its result. If it resolves, we return a dictionary with "fulfilled" status. If it raises an exception, we catch it and return a dictionary with "rejected" status. asyncio.gather() runs all wrapped coroutines concurrently and preserves order, which matches the behavior of Promise.all() in JavaScript.

Go Solution

package main

import (
	"fmt"
)

type Result struct {
	Status string
	Value  any
	Reason string
}

func promiseAllSettled(functions []func() chan any) chan []Result {
	resultChan := make(chan []Result, 1)
	results := make([]Result, len(functions))
	done := make(chan struct{})

	for i, fn := range functions {
		go func(i int, fn func() chan any) {
			defer func() {
				if r := recover(); r != nil {
					results[i] = Result{Status: "rejected", Reason: fmt.Sprintf("%v", r)}
				}
				done <- struct{}{}
			}()
			res := <-fn()
			results[i] = Result{Status: "fulfilled", Value: res}
		}(i, fn)
	}

	go func() {
		for range functions {
			<-done
		}
		resultChan <- results
		close(resultChan)
	}()

	return resultChan
}

In Go, we simulate asynchronous functions using channels. Each function returns a channel with its value. We launch a goroutine for each function, reading from its channel, and populating the results array. We handle panics as rejected results. A final goroutine waits for all tasks to finish and then sends the complete results array through a channel. This approach ensures concurrency and preserves order.

Worked Examples

Example 1

Input: functions = [() => Promise.resolve(15)]

  1. Call the single function to get a promise.
  2. Wrap it: .then() produces {"status": "fulfilled", "value": 15}.
  3. Promise.all() resolves immediately since only one promise exists.
  4. Output: [{"status": "fulfilled", "value": 15}].

Example 2

Input: Two promises resolving at 100ms.

  1. Call both functions concurrently.
  2. Wrap each with .then() producing {"status": "fulfilled", "value": 20} and {"status": "fulfilled", "value": 15}.
  3. Promise.all() resolves after 100ms (the max of the two).
  4. Output: [{"status": "fulfilled", "value": 20}, {"status": "fulfilled", "value": 15}].

Example 3

Input: One promise resolves at 200ms, one rejects at 100ms.

  1. Call both functions concurrently.
  2. Wrap each: first resolves {"status": "fulfilled", "value": 30}, second rejects {"status": "rejected", "reason": "Error"}.
  3. Promise.all() resolves after 200ms (max time).
  4. Output: [{"status": "fulfilled", "value": 30}, {"status": "rejected", "reason": "Error"}].

Complexity Analysis

Measure Complexity Explanation
Time O(t_max) All promises are executed concurrently; total time depends on the longest-running promise
Space O(n) We store one result object per function in the results array

The reasoning is that concurrency allows all promises to run simultaneously, so the algorithm is not slowed down by sequential execution. Space is linear because each function's result is stored individually.

Test Cases

import asyncio

# Single function resolves
assert asyncio.run(promiseAllSettled([lambda: asyncio.sleep(0.1, result=15)])) == [{"status":"fulfilled","value":15}]

# Two functions resolve
assert asyncio.run(promiseAllSettled([
    lambda: asyncio.sleep(0.1, result=20),
    lambda: asyncio.sleep(0.1, result=15)
])) == [
    {"status":"fulfilled","value":20},
    {"status":"fulfilled","value":15}
]

# One resolve, one reject
async def reject_fn():
    raise Exception("Error")

assert asyncio.run(promiseAllSettled([
    lambda: asyncio.sleep(0.2, result=30),
    reject_fn
])) == [
    {"status":"fulfilled","value":30},
    {"status":"rejected","reason":"Error"}
]

# Immediate resolve
assert asyncio.run(promiseAllSettled([lambda: 42])) == [{"status":"fulfilled","value":42}]
Test Why
Single function Validates basic functionality
Two functions Tests concurrency and order preservation
One resolve, one reject Validates rejection handling
Immediate resolve Ensures synchronous values are wrapped correctly

Edge Cases

Single function: If there is only one function, the implementation should still return an array with one result object. Our implementation wraps it and resolves correctly.

Function rejecting immediately: A function may reject without delay. The wrap function catches the exception and returns a rejected object, ensuring the final promise never rejects, which avoids breaking the Promise.all().

Functions with mixed durations: Promises may resolve or reject at different times. The algorithm uses concurrent execution and Promise.all(), so the final result array is guaranteed to preserve the input order regardless of completion time.