Facebook Pixel

2821. Delay the Resolution of Each Promise 🔒

Problem Description

You are given an array of functions called functions where each function returns a promise, and a number ms representing a delay duration in milliseconds.

Your task is to create and return a new array of functions where each function:

  • Corresponds to a function from the original functions array
  • When called, waits for ms milliseconds before executing the original function
  • Returns a promise that resolves or rejects with the same value as the original function's promise

The key requirements are:

  1. Each function in the input array returns a promise
  2. The output array should maintain the same order as the input array
  3. Each new function should add a delay of ms milliseconds before executing its corresponding original function
  4. The delayed promise should preserve the resolution or rejection behavior of the original promise

For example, if you have a function that returns a promise resolving to "hello" immediately, and ms = 1000, the new function should wait 1000 milliseconds, then execute the original function, and return a promise that resolves to "hello".

The solution uses map() to transform each function, creating an async wrapper that:

  • First waits for ms milliseconds using setTimeout wrapped in a Promise
  • Then calls and returns the result of the original function
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The core challenge is to add a delay before each function executes while preserving its promise-based behavior. Since we need to transform each function in the array while maintaining order, map() is the natural choice for iteration.

For adding the delay, we need to wait for a specific amount of time before doing something else. JavaScript's setTimeout is the standard way to introduce delays, but it uses callbacks. To work with promises and modern async patterns, we wrap setTimeout in a Promise: new Promise(resolve => setTimeout(resolve, ms)). This creates a promise that resolves after ms milliseconds.

Since we need to:

  1. First wait for the delay
  2. Then execute the original function
  3. Return the result as a promise

The async/await pattern fits perfectly. We create an async function that:

  • Uses await to pause execution for the delay
  • Then calls the original function and returns its result

The beauty of this approach is that async functions automatically return promises. When we return fn() from an async function, it will:

  • If fn() returns a promise that resolves, our async function's promise resolves with the same value
  • If fn() returns a promise that rejects, our async function's promise rejects with the same error

This natural behavior of async functions means we don't need to manually handle promise resolution or rejection - the async function wrapper automatically preserves the original promise's behavior while adding the delay.

Solution Approach

The implementation uses the map() method to transform each function in the input array into a new delayed version:

function delayAll(functions: Function[], ms: number): Function[] {
    return functions.map(fn => {
        return async function () {
            await new Promise(resolve => setTimeout(resolve, ms));
            return fn();
        };
    });
}

Let's break down the implementation step by step:

  1. Array Transformation with map(): We iterate through the functions array using map(), which creates a new array where each element is a transformed version of the original function.

  2. Creating the Wrapper Function: For each function fn in the original array, we return a new async function. This wrapper function is what gets stored in the result array.

  3. Implementing the Delay: Inside the async function, we create a delay using:

    await new Promise(resolve => setTimeout(resolve, ms))

    This line creates a Promise that resolves after ms milliseconds. The await keyword pauses the function execution until this promise resolves.

  4. Executing the Original Function: After the delay completes, we call the original function with return fn(). Since fn() returns a promise, and we're inside an async function, this will:

    • If fn() resolves with a value, the async wrapper will resolve with the same value
    • If fn() rejects with an error, the async wrapper will reject with the same error
  5. Return Type: The function returns Function[], an array of the new delayed functions. Each function in this array, when called, will execute its delay-then-execute behavior.

The key pattern here is the async function wrapper, which elegantly handles both the delay requirement and the promise preservation without needing explicit promise chaining or error handling code.

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 to understand how the solution works.

Input:

  • functions = [fn1, fn2] where:
    • fn1 returns a promise that resolves to 5 immediately
    • fn2 returns a promise that resolves to 10 immediately
  • ms = 100 (delay of 100 milliseconds)

Step 1: Apply map() transformation

The map() function iterates through each function:

For fn1:

  • Creates a new async function that:
    • First waits 100ms using await new Promise(resolve => setTimeout(resolve, 100))
    • Then calls fn1() and returns its result

For fn2:

  • Creates a new async function that:
    • First waits 100ms using await new Promise(resolve => setTimeout(resolve, 100))
    • Then calls fn2() and returns its result

Step 2: Result array created

The result is [delayedFn1, delayedFn2] where each is an async function.

Step 3: Using the delayed functions

When we call delayedFn1():

  1. Time 0ms: Function starts executing
  2. Time 0-100ms: Waiting during await new Promise(resolve => setTimeout(resolve, 100))
  3. Time 100ms: Delay completes, fn1() is called
  4. Time 100ms+: fn1() returns a promise resolving to 5
  5. The async wrapper returns this promise, which resolves to 5

Visual Timeline:

Call delayedFn1() → Wait 100ms → Execute fn1() → Return promise(5)
        ↑                ↑              ↑              ↑
      t=0ms          t=100ms       t=100ms+      Final result

Handling Rejections:

If fn1 instead returned a promise that rejects with error "failed":

  • After the 100ms delay, fn1() would be called
  • fn1() returns a rejected promise
  • The async wrapper automatically propagates this rejection
  • delayedFn1() returns a promise that rejects with "failed"

This demonstrates how the solution preserves both successful resolutions and rejections while adding the specified delay.

Solution Implementation

1import asyncio
2from typing import List, Callable, Any
3
4def delayAll(functions: List[Callable], ms: int) -> List[Callable]:
5    """
6    Delays the execution of an array of functions by a specified time
7  
8    Args:
9        functions: Array of functions to be delayed
10        ms: Delay time in milliseconds
11  
12    Returns:
13        Array of wrapped async functions that execute after the delay
14    """
15    # Map each function to a delayed async wrapper
16    delayed_functions = []
17  
18    for fn in functions:
19        # Create an async wrapper function for each original function
20        async def create_delayed_function(original_fn=fn):
21            # Wait for the specified delay (convert ms to seconds for asyncio.sleep)
22            await asyncio.sleep(ms / 1000)
23            # Execute and return the result of the original function
24            return original_fn()
25      
26        # Add the wrapped function to the result list
27        delayed_functions.append(create_delayed_function)
28  
29    return delayed_functions
30
1import java.util.List;
2import java.util.ArrayList;
3import java.util.concurrent.CompletableFuture;
4import java.util.concurrent.TimeUnit;
5import java.util.function.Supplier;
6
7/**
8 * Utility class for delaying function execution
9 */
10public class FunctionDelayUtil {
11  
12    /**
13     * Delays the execution of an array of functions by a specified time
14     * @param functions - List of functions to be delayed
15     * @param ms - Delay time in milliseconds
16     * @return List of wrapped suppliers that execute after the delay
17     */
18    public static List<Supplier<CompletableFuture<Object>>> delayAll(
19            List<Supplier<Object>> functions, long ms) {
20      
21        // Create a new list to store the wrapped functions
22        List<Supplier<CompletableFuture<Object>>> delayedFunctions = new ArrayList<>();
23      
24        // Iterate through each function in the input list
25        for (Supplier<Object> fn : functions) {
26            // Create a supplier that returns a CompletableFuture
27            Supplier<CompletableFuture<Object>> delayedSupplier = () -> {
28                // Create a CompletableFuture that completes after the specified delay
29                return CompletableFuture
30                    .delayedExecutor(ms, TimeUnit.MILLISECONDS)
31                    .execute(() -> {})  // Execute empty task after delay
32                    .thenApply(v -> fn.get());  // Then execute the original function
33            };
34          
35            // Alternative implementation using supplyAsync:
36            // Supplier<CompletableFuture<Object>> delayedSupplier = () -> 
37            //     CompletableFuture.supplyAsync(
38            //         fn, 
39            //         CompletableFuture.delayedExecutor(ms, TimeUnit.MILLISECONDS)
40            //     );
41          
42            // Add the wrapped supplier to the result list
43            delayedFunctions.add(delayedSupplier);
44        }
45      
46        // Return the list of delayed functions
47        return delayedFunctions;
48    }
49}
50
1#include <vector>
2#include <functional>
3#include <chrono>
4#include <thread>
5#include <future>
6
7/**
8 * Delays the execution of an array of functions by a specified time
9 * @param functions - Vector of functions to be delayed
10 * @param ms - Delay time in milliseconds
11 * @returns Vector of wrapped async functions that execute after the delay
12 */
13std::vector<std::function<std::future<void>()>> delayAll(
14    const std::vector<std::function<void()>>& functions, 
15    int ms) {
16  
17    // Create a vector to store the wrapped functions
18    std::vector<std::function<std::future<void>()>> delayedFunctions;
19  
20    // Iterate through each function and create a delayed wrapper
21    for (const auto& fn : functions) {
22        // Create a wrapper function that returns a future
23        auto wrappedFunction = [fn, ms]() -> std::future<void> {
24            // Launch async task that delays then executes the function
25            return std::async(std::launch::async, [fn, ms]() {
26                // Wait for the specified delay in milliseconds
27                std::this_thread::sleep_for(std::chrono::milliseconds(ms));
28                // Execute the original function
29                fn();
30            });
31        };
32      
33        // Add the wrapped function to the result vector
34        delayedFunctions.push_back(wrappedFunction);
35    }
36  
37    return delayedFunctions;
38}
39
1/**
2 * Delays the execution of an array of functions by a specified time
3 * @param functions - Array of functions to be delayed
4 * @param ms - Delay time in milliseconds
5 * @returns Array of wrapped async functions that execute after the delay
6 */
7function delayAll(functions: Function[], ms: number): Function[] {
8    return functions.map((fn: Function) => {
9        // Return an async wrapper function for each original function
10        return async function (): Promise<any> {
11            // Wait for the specified delay
12            await new Promise<void>((resolve: () => void) => setTimeout(resolve, ms));
13            // Execute and return the result of the original function
14            return fn();
15        };
16    });
17}
18

Time and Space Complexity

Time Complexity: O(n) where n is the number of functions in the input array. The map operation iterates through each function once to create a new delayed version, performing constant-time operations for each function (creating an async wrapper function).

Space Complexity: O(n) where n is the number of functions in the input array. The function creates a new array of the same size as the input array, with each element being a new async function wrapper. Additionally, each wrapper function creates a closure that captures the original function and the delay value ms, but this doesn't change the overall linear space complexity.

Note on Execution Time: While the creation of the delayed functions has O(n) time complexity, the actual execution of any returned function will have an additional O(1) time complexity plus the delay of ms milliseconds and the execution time of the original function itself.

Common Pitfalls

1. Late Binding Closure Issue in Python

The Python implementation has a critical bug due to late binding in closures. When creating functions inside a loop, the loop variable fn is captured by reference, not by value. This means all delayed functions will reference the last function in the array.

Problem Example:

def delayAll(functions: List[Callable], ms: int) -> List[Callable]:
    delayed_functions = []
  
    for fn in functions:
        async def create_delayed_function():  # Bug: fn is captured by reference
            await asyncio.sleep(ms / 1000)
            return fn()  # All functions will call the last fn
      
        delayed_functions.append(create_delayed_function)
  
    return delayed_functions

Solution: Use default parameter binding or create a closure properly:

def delayAll(functions: List[Callable], ms: int) -> List[Callable]:
    delayed_functions = []
  
    for fn in functions:
        async def create_delayed_function(original_fn=fn):  # Fix: bind fn as default parameter
            await asyncio.sleep(ms / 1000)
            return original_fn()
      
        delayed_functions.append(create_delayed_function)
  
    return delayed_functions

2. Mixing Sync and Async Functions

The problem states functions return promises (async in Python), but the implementation doesn't handle the case where fn() might be an async function itself.

Problem: If fn is async, calling fn() returns a coroutine that needs to be awaited:

async def create_delayed_function(original_fn=fn):
    await asyncio.sleep(ms / 1000)
    return original_fn()  # If original_fn is async, this returns a coroutine, not the result

Solution: Check if the result is a coroutine and await it:

async def create_delayed_function(original_fn=fn):
    await asyncio.sleep(ms / 1000)
    result = original_fn()
    if asyncio.iscoroutine(result):
        return await result
    return result

3. Incorrect Time Unit Conversion

JavaScript's setTimeout uses milliseconds, while Python's asyncio.sleep uses seconds. Forgetting to convert can lead to delays that are 1000x longer than intended.

Problem:

await asyncio.sleep(ms)  # Bug: treats milliseconds as seconds

Solution:

await asyncio.sleep(ms / 1000)  # Correct: converts ms to seconds

4. Not Preserving Function Arguments

The original problem doesn't specify if functions take arguments, but in real-world scenarios, the wrapped functions should accept and forward arguments.

Problem:

async def create_delayed_function(original_fn=fn):
    await asyncio.sleep(ms / 1000)
    return original_fn()  # Cannot pass arguments to original function

Solution:

async def create_delayed_function(original_fn=fn):
    async def wrapper(*args, **kwargs):
        await asyncio.sleep(ms / 1000)
        result = original_fn(*args, **kwargs)
        if asyncio.iscoroutine(result):
            return await result
        return result
    return wrapper
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

How many ways can you arrange the three letters A, B and C?


Recommended Readings

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

Load More