Facebook Pixel

2637. Promise Time Limit

Problem Description

This problem asks you to create a wrapper function that adds a timeout feature to any asynchronous function. You need to implement a timeLimit function that takes two parameters:

  • fn: an asynchronous function that returns a Promise
  • t: a time limit in milliseconds

The timeLimit function should return a new function that behaves as follows:

  1. If the original function completes within the time limit: The new function should resolve with the same result as the original function.

  2. If the original function takes longer than the time limit: The new function should reject with the string "Time Limit Exceeded".

The solution uses Promise.race() to create a race condition between two promises:

  • The original function call fn(...args)
  • A timer promise that automatically rejects after t milliseconds with the message "Time Limit Exceeded"

Whichever promise settles first (either completes or rejects) determines the outcome. If the original function finishes before the timer expires, its result is returned. If the timer expires first, the timeout error is thrown.

For example, if you have a function that takes 150ms to complete but set a time limit of 100ms, the wrapped function will reject with "Time Limit Exceeded" after 100ms, even though the original function is still running.

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

Intuition

When we need to limit the execution time of an asynchronous function, we're essentially creating a competition between two events: the completion of the original function and the expiration of a timer. This naturally leads us to think about racing promises against each other.

The key insight is that JavaScript's Promise.race() method perfectly fits this scenario. It takes an array of promises and returns a new promise that resolves or rejects as soon as the first promise in the array settles. This is exactly what we need - we want to know which happens first: the function completing or the timeout occurring.

To implement this, we need to create two competing promises:

  1. The first promise is simply the original function call: fn(...args)
  2. The second promise is a timer that will automatically reject after t milliseconds

For the timer promise, we use setTimeout() wrapped in a new Promise. The promise constructor takes a resolver and rejector function. We only need the rejector here since we want to throw an error when time runs out. After t milliseconds, we call reject('Time Limit Exceeded').

By putting these two promises in Promise.race(), we get the exact behavior we want:

  • If fn(...args) completes first, its result (whether resolved or rejected) becomes the final result
  • If the timer expires first, the rejection with 'Time Limit Exceeded' becomes the final result

The wrapper function needs to be async and return the result of Promise.race() to maintain the asynchronous nature of the original function while adding the timeout behavior.

Solution Approach

The implementation creates a higher-order function that wraps any asynchronous function with timeout functionality. Here's how the solution works step by step:

  1. Function Signature: The timeLimit function accepts two parameters:

    • fn: The original asynchronous function of type Fn (which accepts any parameters and returns a Promise)
    • t: The timeout duration in milliseconds
  2. Return a New Function: The timeLimit function returns a new async function that accepts the same arguments as the original function using the rest parameter syntax ...args.

  3. Create Racing Promises: Inside the returned function, we use Promise.race() with an array of two promises:

    Promise.race([
        fn(...args),
        new Promise((_, reject) => setTimeout(() => reject('Time Limit Exceeded'), t))
    ])
  4. First Promise - Original Function: fn(...args) calls the original function with all provided arguments. This promise will resolve or reject based on the original function's behavior.

  5. Second Promise - Timeout Timer: We create a new Promise that:

    • Ignores the resolve parameter (using _ as a placeholder)
    • Sets up a timer using setTimeout() that waits for t milliseconds
    • When the timer expires, it calls reject('Time Limit Exceeded')
  6. Race Condition Resolution: Promise.race() will:

    • Return the result of whichever promise settles first
    • If the original function completes before the timeout, its result is returned
    • If the timeout occurs first, the promise rejects with the timeout error message

The beauty of this approach is that it doesn't require canceling the original function or managing complex state. The Promise.race() pattern elegantly handles the timeout logic, and the original function continues running in the background even if the timeout occurs first (though its result will be ignored).

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 timeLimit function works.

Suppose we have an async function that simulates a network request taking 150ms:

async function fetchData(id) {
    await new Promise(resolve => setTimeout(resolve, 150));
    return { data: `Result for ${id}` };
}

Now let's create two wrapped versions with different time limits:

Case 1: Time limit is longer than execution time (200ms limit)

const fastEnough = timeLimit(fetchData, 200);

When we call fastEnough(123):

  1. Two promises start racing:
    • Promise A: fetchData(123) - will complete after 150ms
    • Promise B: Timer promise - will reject after 200ms
  2. At 150ms: Promise A completes with { data: "Result for 123" }
  3. Promise.race() immediately resolves with this result
  4. The timer promise (Promise B) is still pending but ignored
  5. Result: The function successfully returns { data: "Result for 123" }

Case 2: Time limit is shorter than execution time (100ms limit)

const tooSlow = timeLimit(fetchData, 100);

When we call tooSlow(456):

  1. Two promises start racing:
    • Promise A: fetchData(456) - will complete after 150ms
    • Promise B: Timer promise - will reject after 100ms
  2. At 100ms: Promise B rejects with "Time Limit Exceeded"
  3. Promise.race() immediately rejects with this error
  4. The original function (Promise A) continues running but its result is ignored
  5. Result: The function rejects with "Time Limit Exceeded"

Here's the complete flow visualized with timeline:

Time (ms):  0 -------- 100 ------- 150 ------- 200
            |           |          |           |
Case 1:     Start       |          ✓ Success   | Timer expires (ignored)
                        |          Returns data |
                        |                       |
Case 2:     Start       ✗ Timeout  | Original   |
                        Rejects    completes    |
                                  (ignored)      |

This example demonstrates how Promise.race() creates the exact behavior we need - returning whichever outcome happens first, whether it's the function completing or the timeout occurring.

Solution Implementation

1from typing import Callable, Any
2import asyncio
3from functools import wraps
4
5# Type definition for an asynchronous function that accepts any parameters and returns a coroutine
6Fn = Callable[..., Any]
7
8def timeLimit(fn: Fn, t: int) -> Fn:
9    """
10    Creates a time-limited version of an asynchronous function.
11    If the function doesn't complete within the specified time limit, it will raise a TimeoutError.
12  
13    Args:
14        fn: The asynchronous function to wrap with a time limit
15        t: The time limit in milliseconds
16  
17    Returns:
18        A new function with the same signature as the input function, but with a time limit
19    """
20    @wraps(fn)
21    async def time_limited_function(*args: Any, **kwargs: Any) -> Any:
22        # Convert milliseconds to seconds for asyncio.wait_for
23        timeout_seconds = t / 1000
24      
25        try:
26            # Execute the original function with timeout
27            # asyncio.wait_for will raise TimeoutError if the function doesn't complete in time
28            result = await asyncio.wait_for(
29                fn(*args, **kwargs), 
30                timeout=timeout_seconds
31            )
32            return result
33        except asyncio.TimeoutError:
34            # Raise an exception with the same message as the TypeScript version
35            raise Exception('Time Limit Exceeded')
36  
37    return time_limited_function
38
39# Example usage:
40# async def example():
41#     async def delay_function(t):
42#         await asyncio.sleep(t / 1000)
43#         return "Completed"
44#     
45#     limited = timeLimit(delay_function, 100)
46#     try:
47#         result = await limited(150)
48#     except Exception as e:
49#         print(e)  # "Time Limit Exceeded" at t=100ms
50
1import java.util.concurrent.*;
2import java.util.function.Function;
3
4/**
5 * Class providing time-limited execution functionality for asynchronous operations
6 */
7public class TimeLimitFunction {
8  
9    /**
10     * Creates a time-limited version of an asynchronous function.
11     * If the function doesn't complete within the specified time limit, it will throw a TimeoutException.
12     * 
13     * @param fn The asynchronous function to wrap with a time limit (represented as a Function that returns a CompletableFuture)
14     * @param t The time limit in milliseconds
15     * @param <T> The type of the input parameter
16     * @param <R> The type of the result
17     * @return A new function with the same signature as the input function, but with a time limit
18     */
19    public static <T, R> Function<T, CompletableFuture<R>> timeLimit(
20            Function<T, CompletableFuture<R>> fn, long t) {
21      
22        return (T args) -> {
23            // Create a CompletableFuture that will complete with the result of the original function
24            CompletableFuture<R> originalFuture = fn.apply(args);
25          
26            // Create a CompletableFuture that will complete exceptionally after the timeout
27            CompletableFuture<R> timeoutFuture = new CompletableFuture<>();
28          
29            // Schedule the timeout to occur after the specified time limit
30            ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
31            scheduler.schedule(() -> {
32                timeoutFuture.completeExceptionally(new TimeoutException("Time Limit Exceeded"));
33            }, t, TimeUnit.MILLISECONDS);
34          
35            // Race between the original function and the timeout
36            // Returns whichever completes first
37            return CompletableFuture.anyOf(originalFuture, timeoutFuture)
38                    .thenCompose(result -> {
39                        // Shutdown the scheduler to clean up resources
40                        scheduler.shutdown();
41                      
42                        // If originalFuture completed first, return its result
43                        if (originalFuture.isDone() && !originalFuture.isCompletedExceptionally()) {
44                            return originalFuture;
45                        }
46                        // Otherwise, the timeout occurred first
47                        return timeoutFuture;
48                    });
49        };
50    }
51  
52    /**
53     * Example usage demonstrating the time limit functionality
54     */
55    public static void main(String[] args) {
56        // Create a function that delays for a specified time
57        Function<Long, CompletableFuture<String>> delayFunction = (Long delay) -> 
58            CompletableFuture.supplyAsync(() -> {
59                try {
60                    Thread.sleep(delay);
61                    return "Completed successfully";
62                } catch (InterruptedException e) {
63                    throw new RuntimeException(e);
64                }
65            });
66      
67        // Create a time-limited version with 100ms timeout
68        Function<Long, CompletableFuture<String>> limited = timeLimit(delayFunction, 100);
69      
70        // This will timeout because 150ms > 100ms limit
71        limited.apply(150L)
72            .exceptionally(ex -> {
73                System.out.println(ex.getMessage()); // "Time Limit Exceeded"
74                return null;
75            });
76    }
77}
78
1#include <functional>
2#include <future>
3#include <chrono>
4#include <stdexcept>
5#include <thread>
6
7// Type definition for an asynchronous function that accepts any parameters and returns a future
8template<typename ReturnType, typename... Args>
9using Fn = std::function<std::future<ReturnType>(Args...)>;
10
11/**
12 * Creates a time-limited version of an asynchronous function.
13 * If the function doesn't complete within the specified time limit, it will throw an exception.
14 * 
15 * @param fn - The asynchronous function to wrap with a time limit
16 * @param t - The time limit in milliseconds
17 * @returns A new function with the same signature as the input function, but with a time limit
18 */
19template<typename ReturnType, typename... Args>
20Fn<ReturnType, Args...> timeLimit(Fn<ReturnType, Args...> fn, int t) {
21    return [fn, t](Args... args) -> std::future<ReturnType> {
22        // Create a promise-future pair for managing the result
23        auto promise = std::make_shared<std::promise<ReturnType>>();
24        std::future<ReturnType> future = promise->get_future();
25      
26        // Launch the original function asynchronously
27        std::thread([promise, fn, t](Args... args) {
28            try {
29                // Get the future from the original function
30                std::future<ReturnType> original_future = fn(args...);
31              
32                // Wait for the result with a timeout
33                if (original_future.wait_for(std::chrono::milliseconds(t)) == std::future_status::ready) {
34                    // Function completed within time limit, set the result
35                    promise->set_value(original_future.get());
36                } else {
37                    // Time limit exceeded, set an exception
38                    promise->set_exception(std::make_exception_ptr(
39                        std::runtime_error("Time Limit Exceeded")
40                    ));
41                }
42            } catch (...) {
43                // Forward any exceptions from the original function
44                promise->set_exception(std::current_exception());
45            }
46        }, args...).detach();
47      
48        return future;
49    };
50}
51
52/**
53 * Example usage:
54 * auto delay_fn = [](int delay_ms) -> std::future<void> {
55 *     return std::async(std::launch::async, [delay_ms]() {
56 *         std::this_thread::sleep_for(std::chrono::milliseconds(delay_ms));
57 *     });
58 * };
59 * 
60 * Fn<void, int> limited = timeLimit<void, int>(delay_fn, 100);
61 * auto result = limited(150);
62 * try {
63 *     result.get(); // Will throw "Time Limit Exceeded" at t=100ms
64 * } catch (const std::exception& e) {
65 *     std::cout << e.what() << std::endl;
66 * }
67 */
68
1// Type definition for an asynchronous function that accepts any parameters and returns a Promise
2type Fn = (...params: any[]) => Promise<any>;
3
4/**
5 * Creates a time-limited version of an asynchronous function.
6 * If the function doesn't complete within the specified time limit, it will reject with an error.
7 * 
8 * @param fn - The asynchronous function to wrap with a time limit
9 * @param t - The time limit in milliseconds
10 * @returns A new function with the same signature as the input function, but with a time limit
11 */
12function timeLimit(fn: Fn, t: number): Fn {
13    return async function (...args: any[]): Promise<any> {
14        // Race between the original function and a timeout promise
15        return Promise.race([
16            // Execute the original function with the provided arguments
17            fn(...args),
18            // Create a promise that rejects after the specified time limit
19            new Promise((_, reject) => 
20                setTimeout(() => reject('Time Limit Exceeded'), t)
21            ),
22        ]);
23    };
24}
25
26/**
27 * Example usage:
28 * const limited = timeLimit((t) => new Promise(res => setTimeout(res, t)), 100);
29 * limited(150).catch(console.log) // "Time Limit Exceeded" at t=100ms
30 */
31

Time and Space Complexity

Time Complexity: O(1)

The timeLimit function itself performs a constant amount of work regardless of input size. It creates and returns a new function that sets up a Promise.race between the original function call and a timeout. The setup operations (creating promises, setting timeout) all execute in constant time. The actual execution time depends on the wrapped function fn, but the overhead introduced by the wrapper is constant.

Space Complexity: O(1)

The space complexity is constant with respect to the wrapper implementation. The function allocates:

  • A fixed amount of space for the returned async function closure
  • Two promises (one for the original function call, one for the timeout)
  • A timeout handle in the event loop

These allocations don't scale with any input size parameter. While the wrapped function fn may have its own space complexity, the wrapper itself only adds a constant amount of additional space overhead.

Common Pitfalls

1. Resource Cleanup and Memory Leaks

The original function continues running even after the timeout occurs. This can lead to:

  • Unclosed resources (database connections, file handles, network sockets)
  • Memory leaks from long-running operations
  • Unexpected side effects completing after timeout

Example Problem:

async def problematic_function():
    connection = await create_database_connection()
    await asyncio.sleep(5)  # Long operation
    result = await connection.query("SELECT * FROM users")
    await connection.close()  # Never reached if timeout occurs!
    return result

limited = timeLimit(problematic_function, 1000)  # 1 second timeout
# Connection remains open after timeout!

Solution: Use context managers or try-finally blocks:

async def safe_function():
    connection = None
    try:
        connection = await create_database_connection()
        await asyncio.sleep(5)
        result = await connection.query("SELECT * FROM users")
        return result
    finally:
        if connection:
            await connection.close()

2. Cancellation vs Abandonment

The current implementation doesn't actually cancel the original task - it just abandons it. The original function keeps executing in the background, consuming resources.

Solution: Properly cancel the task:

def timeLimit(fn: Fn, t: int) -> Fn:
    @wraps(fn)
    async def time_limited_function(*args: Any, **kwargs: Any) -> Any:
        timeout_seconds = t / 1000
      
        # Create task to enable cancellation
        task = asyncio.create_task(fn(*args, **kwargs))
      
        try:
            result = await asyncio.wait_for(task, timeout=timeout_seconds)
            return result
        except asyncio.TimeoutError:
            # Cancel the task to prevent it from continuing
            task.cancel()
            try:
                await task
            except asyncio.CancelledError:
                pass
            raise Exception('Time Limit Exceeded')
  
    return time_limited_function

3. Non-Cancellable Operations

Some operations (like synchronous blocking I/O or CPU-bound tasks) cannot be cancelled by asyncio and will continue running despite cancellation attempts.

Example Problem:

async def cpu_intensive():
    # This blocks the event loop and can't be cancelled
    result = complex_synchronous_calculation()
    return result

Solution: Use thread/process pools for blocking operations:

async def cpu_intensive():
    loop = asyncio.get_event_loop()
    # Run in thread pool to make it cancellable
    result = await loop.run_in_executor(None, complex_synchronous_calculation)
    return result

4. Error Message Consistency

Different implementations might use different error types or messages, breaking compatibility when switching between JavaScript/TypeScript and Python versions.

Solution: Define consistent error handling:

class TimeLimitExceeded(Exception):
    def __init__(self):
        super().__init__('Time Limit Exceeded')

# Use consistent error type
raise TimeLimitExceeded()
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Consider the classic dynamic programming of longest increasing subsequence:

Find the length of the longest subsequence of a given sequence such that all elements of the subsequence are sorted in increasing order.

For example, the length of LIS for [50, 3, 10, 7, 40, 80] is 4 and LIS is [3, 7, 40, 80].

What is the recurrence relation?


Recommended Readings

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

Load More