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 Promiset
: a time limit in milliseconds
The timeLimit
function should return a new function that behaves as follows:
-
If the original function completes within the time limit: The new function should resolve with the same result as the original function.
-
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.
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:
- The first promise is simply the original function call:
fn(...args)
- 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:
-
Function Signature: The
timeLimit
function accepts two parameters:fn
: The original asynchronous function of typeFn
(which accepts any parameters and returns a Promise)t
: The timeout duration in milliseconds
-
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
. -
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)) ])
-
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. -
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 fort
milliseconds - When the timer expires, it calls
reject('Time Limit Exceeded')
- Ignores the resolve parameter (using
-
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 EvaluatorExample 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)
:
- Two promises start racing:
- Promise A:
fetchData(123)
- will complete after 150ms - Promise B: Timer promise - will reject after 200ms
- Promise A:
- At 150ms: Promise A completes with
{ data: "Result for 123" }
- Promise.race() immediately resolves with this result
- The timer promise (Promise B) is still pending but ignored
- 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)
:
- Two promises start racing:
- Promise A:
fetchData(456)
- will complete after 150ms - Promise B: Timer promise - will reject after 100ms
- Promise A:
- At 100ms: Promise B rejects with
"Time Limit Exceeded"
- Promise.race() immediately rejects with this error
- The original function (Promise A) continues running but its result is ignored
- 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()
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
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 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
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!