61. Implement a configurable debounce utility.
Implement debounce(fn, wait). Return a normal function that forwards its latest call-time this and arguments, invokes fn only after wait milliseconds have elapsed without another call, and exposes cancel() and flush() methods. cancel() must prevent the pending invocation and release retained references; flush() must immediately run a pending invocation and return its result, or return the most recent completed result when nothing is pending. Reject a non-function or a negative or non-finite wait, use browser timers, and do not use a library. With a deterministic fake clock, calls at 0, 20, and 40 ms with wait = 50 must invoke once at 90 ms with the third call's arguments.
I would use one browser timer and keep the latest call state inside a closure. Each call saves the newest this and arguments, clears the previous timer, and starts a fresh timer for wait milliseconds. When the timer finally fires, I call fn with that latest state and save its result. cancel() removes pending work. flush() runs pending work immediately or returns the last completed result. Each operation is O(1) time with O(1) auxiliary space.
See the Code while reading this explanation.
The goal is to delay a function until calls have stopped for the requested amount of time. Every new call replaces the earlier pending call. We remember only the newest this value and arguments, plus the result from the most recent completed invocation. One browser timeout tracks the pending work. The returned function also has cancel() to discard pending work and release references, and flush() to run pending work immediately. This design matches the required trailing-edge debounce behavior with a small, fixed amount of state.
- Should
flush()return the most recent completed result when there is no pending call? Yes, that is required here. - Should
cancel()keep the most recent completed result? Yes. It clears pending state but leaves the completed result available toflush().
debounce(fn, wait) receives a function and a delay. It returns a normal function. That returned function must forward the latest call-time this and arguments. The original fn runs only after there have been no new calls for wait milliseconds.
The returned function also exposes cancel() and flush(). cancel() prevents pending work from running and releases retained references. flush() immediately runs pending work and returns its result. If nothing is pending, flush() returns the most recent completed result.
The input is validated first. fn must be a function. wait must be a finite number and cannot be negative.
The closure stores timerId, lastArgs, lastThis, lastResult, and hasPending. timerId identifies the active browser timeout. lastArgs and lastThis belong to the newest call. lastResult stores the most recent completed result. hasPending tells us whether a call is waiting to run.
The main invariant is that while work is pending, lastArgs and lastThis always belong to the newest call.
When the returned function is called, it saves the current this and arguments. It marks the call as pending. If an earlier timeout exists, that timeout is cleared. A new timeout is then created for wait milliseconds.
This reset is the key debounce behavior. Every new call starts the quiet period again.
At 0 ms, the wrapper is called with A. It saves A and starts a timer that would fire at 50 ms.
At 20 ms, the wrapper is called with B. The first timer is cleared. B becomes the newest saved call, and a new timer is scheduled for 70 ms.
At 40 ms, the wrapper is called with C. The second timer is cleared. C becomes the newest saved call, and a new timer is scheduled for 90 ms.
There are no more calls. At 90 ms, 50 ms have passed since the call at 40 ms. fn runs exactly once with the third call's this and arguments. Its return value becomes lastResult. The pending references are then released.
cancel() and flush()cancel() clears the active timeout when one exists. It also clears the saved this and arguments and marks the operation as not pending. It does not call fn. The previously completed lastResult is kept.
flush() first checks whether work is pending. If nothing is pending, it returns lastResult. If work is pending, it prevents the scheduled timeout from firing later, invokes fn immediately with the newest saved context and arguments, stores that result, clears the pending references, and returns the result.
Only the newest call's context and arguments are retained. Every new call resets the timeout, so fn can run only after a full wait interval with no later call. cancel() removes pending work before it can run. flush() executes exactly the newest pending call immediately and prevents the timer from executing that call again.
Each normal call, cancel(), and flush() performs a constant amount of state and timer work. Each operation is O(1) time. The closure stores a fixed number of variables, so auxiliary space is O(1).
Use a trailing-edge debounce with one browser timeout and a fixed set of closure variables. On every wrapper call, replace the saved this and arguments with the newest values, clear the previous timeout, and create a fresh timeout for wait milliseconds. The central invariant is that pending state always represents the most recent call. When that timeout finally completes, invoke fn with the saved context and arguments, store its returned value, and release the pending references. cancel() discards pending work, while flush() executes pending work immediately or returns the latest completed result.
function debounce(fn, wait) {
// The wrapped value must be callable.
if (typeof fn !== 'function') {
throw new TypeError('fn must be a function');
}
// The delay must be a finite, non-negative number.
if (typeof wait !== 'number' || !Number.isFinite(wait) || wait < 0) {
throw new RangeError('wait must be a finite non-negative number');
}
// Keep one pending timer and the state from the newest call.
let timerId = null;
let lastArgs = null;
let lastThis = null;
let lastResult;
let hasPending = false;
function invoke() {
// A flush may call invoke() before the browser timer fires.
// Cancel that timer so the same pending call cannot run twice.
if (timerId !== null) {
clearTimeout(timerId);
timerId = null;
}
// Copy the newest call state before releasing retained references.
const args = lastArgs;
const thisArg = lastThis;
// The pending call is now being consumed.
lastArgs = null;
lastThis = null;
hasPending = false;
// Forward the newest call-time this value and arguments.
const result = fn.apply(thisArg, args);
// Keep the latest completed result for future flush() calls.
lastResult = result;
return result;
}
function debounced(...args) {
// Every new call replaces the previous pending call state.
lastThis = this;
lastArgs = args;
hasPending = true;
// Restart the quiet period when another call arrives.
if (timerId !== null) {
clearTimeout(timerId);
}
// Browser setTimeout schedules the trailing invocation.
timerId = setTimeout(invoke, wait);
// Before the first completed invocation this is undefined.
// Later calls return the most recent completed result.
return lastResult;
}
debounced.cancel = function cancel() {
// Prevent any pending browser timeout from invoking fn.
if (timerId !== null) {
clearTimeout(timerId);
}
// Release all references belonging to the pending call.
timerId = null;
lastArgs = null;
lastThis = null;
hasPending = false;
};
debounced.flush = function flush() {
// With no pending work, return the latest completed result.
if (!hasPending) {
return lastResult;
}
// Run the newest pending call immediately.
return invoke();
};
return debounced;
}
// Example matching the diagram.
// Calls happen at about 0 ms, 20 ms, and 40 ms with wait = 50 ms.
// With a deterministic fake clock, fn runs exactly once at 90 ms with "C".
const startTime = performance.now();
const debounced = debounce(function (value) {
const elapsed = Math.round(performance.now() - startTime);
console.log(`fn invoked with ${value} at about ${elapsed} ms`);
return value;
}, 50);
debounced('A');
setTimeout(() => debounced('B'), 20);
setTimeout(() => debounced('C'), 40);Each call to the debounced function does a fixed amount of work. It saves a few values, may clear one timeout, and creates one timeout. cancel() and flush() also do a fixed amount of work. Therefore each operation is O(1) time. The closure stores only one timer identifier, the latest arguments, the latest this, the latest completed result, and one pending flag. That storage does not grow as more calls arrive, so auxiliary space is O(1).
Debouncing is useful when an event can happen many times quickly but expensive work should happen only after activity stops. Frontend examples include search input requests, form validation, resize handling, autosaving after typing pauses, and delaying other work until the user stops changing an input.
This problem tests closures, browser timers, dynamic this, argument forwarding, and careful state management. It also checks whether a candidate can design a small API with precise cancel() and flush() behavior. A strong solution must reset the timer correctly, retain only the newest call, release pending references, prevent a flushed call from running twice, validate inputs, and explain the O(1) per-operation time and O(1) auxiliary space accurately.
One common mistake is starting a new timeout without clearing the previous one, which can invoke fn several times. Another is returning an arrow function and accidentally losing the caller's dynamic this. Candidates may also forget to replace the saved arguments on every call, fail to release pending references in cancel(), or let flush() invoke the pending call without cancelling its scheduled timeout. Another mistake is returning undefined from flush() when nothing is pending instead of returning the most recent completed result. Invalid waits such as negative numbers, NaN, and Infinity must also be rejected.
State the invariant before writing the methods: while work is pending, the saved this and arguments always belong to the newest call. Then show that every new call resets the single timer. This makes the behavior of cancel() and flush() easy to reason about.










