Sometimes you have a long running task, and you may wish to cancel it before it completes. To help with this goal, write a function cancellable that accepts a generator object and returns an array of two values: a cancel function and a promise.
You may assume the generator function will only yield promises. It is your function's responsibility to pass the values resolved by the promise back to the generator. If the promise rejects, your function should throw that error back to the generator.
If the cancel callback is called before the generator is done, your function should throw an error back to the generator. That error should be the string "Cancelled" (Not an Error object). If the error was caught, the returned promise should resolve with the next value that was yielded or returned. Otherwise, the promise should reject with the thrown error. No more code should be executed.
When the generator is done, the promise your function returned should resolve the value the generator returned. If, however, the generator throws an error, the returned promise should reject with the error.
An example of how your code would be used:
function* tasks() {
const val = yield new Promise(resolve => resolve(2 + 2));
yield new Promise(resolve => setTimeout(resolve, 100));
return val + 1; // calculation shouldn't be done.
}
const [cancel, promise] = cancellable(tasks());
setTimeout(cancel, 50);
promise.catch(console.log); // logs "Cancelled" at t=50ms
If instead cancel() was not called or was called after t=100ms, the promise would have resolved 5.
Example 1:
Input:
generatorFunction = function*() {
return 42;
}
cancelledAt = 100
Output: {"resolved": 42}
Explanation:
const generator = generatorFunction();
const [cancel, promise] = cancellable(generator);
setTimeout(cancel, 100);
promise.then(console.log); // resolves 42 at t=0ms
The generator immediately yields 42 and finishes. Because of that, the returned promise immediately resolves 42. Note that cancelling a finished generator does nothing.
Example 2:
Input:
generatorFunction = function*() {
const msg = yield new Promise(res => res("Hello"));
throw `Error: ${msg}`;
}
cancelledAt = null
Output: {"rejected": "Error: Hello"}
Explanation:
A promise is yielded. The function handles this by waiting for it to resolve and then passes the resolved value back to the generator. Then an error is thrown which has the effect of causing the promise to reject with the same thrown error.
Example 3:
Input:
generatorFunction = function*() {
yield new Promise(res => setTimeout(res, 200));
return "Success";
}
cancelledAt = 100
Output: {"rejected": "Cancelled"}
Explanation:
While the function is waiting for the yielded promise to resolve, cancel() is called. This causes an error message to be sent back to the generator. Since this error is uncaught, the returned promise rejected with this error.
Example 4:
Input:
generatorFunction = function*() {
let result = 0;
yield new Promise(res => setTimeout(res, 100));
result += yield new Promise(res => res(1));
yield new Promise(res => setTimeout(res, 100));
result += yield new Promise(res => res(1));
return result;
}
cancelledAt = null
Output: {"resolved": 2}
Explanation:
4 promises are yielded. Two of those promises have their values added to the result. After 200ms, the generator finishes with a value of 2, and that value is resolved by the returned promise.
Example 5:
Input:
generatorFunction = function*() {
let result = 0;
try {
yield new Promise(res => setTimeout(res, 100));
result += yield new Promise(res => res(1));
yield new Promise(res => setTimeout(res, 100));
result += yield new Promise(res => res(1));
} catch(e) {
return result;
}
return result;
}
cancelledAt = 150
Output: {"resolved": 1}
Explanation:
The first two yielded promises resolve and cause the result to increment. However, at t=150ms, the generator is cancelled. The error sent to the generator is caught and the result is returned and finally resolved by the returned promise.
Example 6:
Input:
generatorFunction = function*() {
try {
yield new Promise((resolve, reject) => reject("Promise Rejected"));
} catch(e) {
let a = yield new Promise(resolve => resolve(2));
let b = yield new Promise(resolve => resolve(2));
return a + b;
};
}
cancelledAt = null
Output: {"resolved": 4}
Explanation:
The first yielded promise immediately rejects. This error is caught. Because the generator hasn't been cancelled, execution continues as usual. It ends up resolving 2 + 2 = 4.
Constraints:
cancelledAt == null or 0 <= cancelledAt <= 1000generatorFunction returns a generator objectProblem Overview: You need to implement a wrapper around a generator that yields asynchronous tasks. Each yielded value is a Promise. The wrapper executes them sequentially and exposes a cancel() function. If cancellation happens, execution stops immediately and the returned promise rejects with "Cancelled".
The challenge is coordinating asynchronous execution while allowing external interruption. You must resume the generator when a promise resolves and propagate errors correctly when cancellation occurs.
Approach 1: Manual Promise and Exception Handling (O(k) time, O(1) space)
This approach manually drives the generator using next() and throw(). After each yielded promise resolves, you call generator.next(value). If it rejects, propagate the error using generator.throw(error). Cancellation is handled by setting a shared flag and rejecting the controlling promise immediately. This approach mirrors how async runtimes internally schedule generators and gives full control over promise chaining.
Approach 2: Async/Await Driven Task Scheduler (O(k) time, O(1) space)
An async function loops through generator outputs and awaits each yielded promise. After resolution, pass the value back into the generator using next(). If cancellation is triggered, throw a "Cancelled" error which propagates to the caller. The key insight is that async/await simplifies promise orchestration while still allowing interruption checks between steps. This is usually the cleanest implementation for JavaScript environments using Promises.
Approach 3: Asynchronous Iteration with Cancellation (O(k) time, O(1) space)
Treat generator progression as an asynchronous iteration process. Each iteration waits for the previous promise to settle before requesting the next generator value. Cancellation injects an error into the generator and stops further iteration. This model aligns well with asynchronous programming patterns and keeps execution state minimal.
Approach 4: Error Propagation with Immediate Cancellation (O(k) time, O(1) space)
Instead of checking a flag on every step, cancellation immediately rejects the outer promise and forces the generator to terminate by throwing an error. Promise resolution handlers verify whether execution has already been cancelled before continuing. This approach emphasizes fast termination and clean error propagation through the promise chain.
Recommended for interviews: The async/await scheduler approach is the most readable and demonstrates strong understanding of JavaScript async control flow. Interviewers often expect candidates to reason about generator execution and promise chaining. Starting with a manual promise-driven solution shows understanding of how generators interact with asynchronous code, while the async/await version shows practical engineering judgment.
This approach leverages the power of async/await to handle the promises yielded by the generator function. The idea is to iterate over each yield of the generator, wait for the promise to resolve, and then send the result back to the generator. If a cancellation signal is received, an error is thrown to the generator to terminate its execution. Ultimately, the promise returned by our cancellable function resolves or rejects based on the generator's completion.
The cancellable function is designed to manage generator execution. Inside, a cancel flag tracks cancellation requests. The function returns an array comprising a cancelFunc closure to record cancellation desires and a promise handling the generator's lifecycle.
To process each step of the generator, we use an internal step function. This function checks if cancellation was requested and, if so, attempts to throw an error into the generator to stop execution. Otherwise, it proceeds to handle generator yields, using promises to gradually pass resolved values back—and handling any rejections by throwing them into the generator.
JavaScript
Time Complexity: O(n) dealing with n promises; each one must be awaited.
Space Complexity: O(1) since it controls one generator at a time (sequential operation).
In this approach, we use a more manual handling of the promises and exceptions that occur during the generator execution process. We manually advance the generator using the next() and throw() methods. This method provides better control over the promise resolution and rejection process, handling cancellation with focused precision.
This implementation of cancellable employs a promise to control flow through the generator. It defines a cancelFunc that updates a cancel state, which, when set to true, triggers terminating error delivery to the generator. The promise endeavors to manage the lifecycle similar to step and handleNext, carefully catching and rethrowing errors as necessary.
JavaScript
Time Complexity: O(n) focusing on n sequential promise resolutions.
Space Complexity: O(1) operates within constant space boundaries aside from iterated state tracking.
This approach involves iterating through the generator asynchronously and allowing for an early exit by injecting a cancellation error if the cancel function is called. We will maintain references to the current promise and handle both resolutions and rejections.
The cancellable function initializes a generator iterator and processes each yielded promise iteratively. The cancel function sets a flag cancelRequested. The step function performs the core logic: checking if cancellation is required, resolving promises, and handling rejections to potentially reinject them back into the generator. Upon completion, it resolves or rejects the associated promise as per the final generator state.
JavaScript
Time Complexity: O(n), where n is the number of yield points in the generator.
Space Complexity: O(1), we use a single promise at any given time.
This approach heavily focuses on the immediate handling of errors, specifically cancellation. By catching any thrown cancellations early, we can halt further processing more effectively by actively managing the iterative generator operations.
This solution builds on early error detection and handling, especially when dealing with cancellation. At each step, we check if the cancellation is requested and, if so, inject a "Cancelled" exception directly into the generator to cease execution. This provides efficient control over the lifecycle of the generator without allowing further steps to proceed post-cancellation.
JavaScript
Time Complexity: O(n), where n is the number of yield points in the generator.
Space Complexity: O(1), maintaining a single promise at every point.
TypeScript
| Approach | Complexity |
|---|---|
| Async/Await Driven Task Scheduler | Time Complexity: O(n) dealing with n promises; each one must be awaited. Space Complexity: O(1) since it controls one generator at a time (sequential operation). |
| Manual Promise and Exception Handling | Time Complexity: O(n) focusing on n sequential promise resolutions. Space Complexity: O(1) operates within constant space boundaries aside from iterated state tracking. |
| Asynchronous Iteration with Cancellation | Time Complexity: O(n), where n is the number of yield points in the generator. |
| Error Propagation with Immediate Cancellation | Time Complexity: O(n), where n is the number of yield points in the generator. |
| Default Approach | — |
| Approach | Time | Space | When to Use |
|---|---|---|---|
| Manual Promise and Exception Handling | O(k) | O(1) | When you want full control over generator progression and promise resolution. |
| Async/Await Driven Task Scheduler | O(k) | O(1) | Preferred for readability and typical JavaScript async workflows. |
| Asynchronous Iteration with Cancellation | O(k) | O(1) | Useful when modeling the generator execution as a controlled async iteration loop. |
| Error Propagation with Immediate Cancellation | O(k) | O(1) | Best when cancellation must immediately stop execution and reject pending tasks. |
LeetCode was HARD until I Learned these 15 Patterns • Ashish Pratap Singh • 1,002,262 views views
Watch 9 more video solutions →Practice Design Cancellable Function with our built-in code editor and test cases.
Practice on FleetCodePractice this problem
Open in Editor