Watch 10 video solutions for Design Cancellable Function, a hard level problem. This walkthrough by Ashish Pratap Singh has 1,002,262 views views. Want to try solving it yourself? Practice on FleetCode or read the detailed text solution.
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.
| 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. |