Harden debounce with cancel and flush
Build debounce with cancel and flush. The interviewer expects a small, reusable utility with clear behavior under repeated calls and invalid inputs.
Answer Strategy
For debounce with cancel and flush, start by stating the public contract before writing code: argument shape, return shape, mutation rules, error behavior, and whether work is synchronous, timed, cached, or cancellable.
A senior solution uses boring names for hidden state. If the function stores a timer, cache entry, listener, or in-flight promise, say who owns that state and how it is cleaned up.
After the baseline passes, harden the edge cases: empty input, repeated calls, invalid values, thrown callbacks, stable ordering, and memory lifetime. The reference below is written to be narrated line by line.
Reference Implementation: Debounce With Cancel And Flush
Debounce stores behavior between calls, so the public API includes cleanup methods for unmounts and form submission.
type Debounced<TArgs extends unknown[]> = {
(...args: TArgs): void;
cancel(): void;
flush(): void;
};
function debounce<TArgs extends unknown[]>(
fn: (...args: TArgs) => void,
delayMs: number
): Debounced<TArgs> {
let timer: ReturnType<typeof setTimeout> | null = null;
let lastArgs: TArgs | null = null;
function cancel() {
if (timer !== null) clearTimeout(timer);
timer = null;
lastArgs = null;
}
function flush() {
if (timer === null || lastArgs === null) return;
const args = lastArgs;
cancel();
fn(...args);
}
function debounced(...args: TArgs) {
lastArgs = args;
if (timer !== null) clearTimeout(timer);
timer = setTimeout(() => {
timer = null;
const nextArgs = lastArgs;
lastArgs = null;
if (nextArgs) fn(...nextArgs);
}, delayMs);
}
debounced.cancel = cancel;
debounced.flush = flush;
return debounced;
}Runnable Playground
Edit the implementation and run the tests directly in the browser. For system design questions, the playground focuses on the core state/data logic that the UI would call.
function debounce(fn: (...args: any[]) => void, delayMs: number) {
let timer: ReturnType<typeof setTimeout> | null = null;
let lastArgs: any[] | null = null;
function cancel() {
if (timer !== null) clearTimeout(timer);
timer = null;
lastArgs = null;
}
function flush() {
if (timer === null || lastArgs === null) return;
const args = lastArgs;
cancel();
fn(...args);
}
function debounced(...args: any[]) {
lastArgs = args;
if (timer !== null) clearTimeout(timer);
timer = setTimeout(() => {
timer = null;
const argsToUse = lastArgs;
lastArgs = null;
if (argsToUse) fn(...argsToUse);
}, delayMs);
}
debounced.cancel = cancel;
debounced.flush = flush;
return debounced;
}Testing Strategy
Convert the answer into observable behavior. In a mid-senior interview, say which behaviors are covered by unit tests, interaction tests, accessibility checks, and one browser smoke path.
vi.useFakeTimers();
test('debounce keeps only the latest call and can flush', () => {
const calls: string[] = [];
const save = debounce((value: string) => calls.push(value), 100);
save('r');
save('re');
save('rea');
expect(calls).toEqual([]);
save.flush();
expect(calls).toEqual(['rea']);
save('react');
save.cancel();
vi.advanceTimersByTime(100);
expect(calls).toEqual(['rea']);
});Interviewer Signal
Tests whether you can turn a familiar utility into a precise contract instead of coding only the happy path.
Constraints
- Define the function signature before coding.
- Do not rely on global mutable state unless it is part of the returned closure.
- Explain time and space cost for the common path.
Model Answer Shape
- Write the smallest public contract first.
- Cover empty input, repeated calls, thrown errors, and cleanup behavior.
- Keep implementation readable enough to narrate under interview pressure.
Tradeoffs
- A compact implementation is attractive, but explicit state names are easier to debug live.
- Supporting every possible input can distract from the core contract; state the scope before coding.
Edge Cases
- No arguments or undefined callbacks.
- Synchronous throw inside the wrapped function.
- Repeated calls before the previous result settles.
Testing And Proof
- Happy path with representative inputs.
- Boundary input and repeated invocation.
- Cleanup or cancellation if timers or promises are involved.
Follow-Ups
- How would you expose cancellation?
- How would the API change for React usage?