Event handlers for search boxes, resize listeners, and other fast-changing UI signals can fire many times before the user has finished the action. A debounce function keeps that noisy input from calling expensive code until activity has been quiet for a chosen delay.
The wrapper keeps one timer in a closure. Every call cancels the previous timer with clearTimeout() and starts a new one with setTimeout(), so only the latest arguments reach the callback when the timer expires.
Use this pattern for delayed work such as search requests, validation, draft saving, or layout recalculation after a burst of input. A terminal smoke test can run in Node.js to prove the timer behavior, and the same helper can wrap browser handlers registered with addEventListener().
function debounce(callback, delay = 300) { let timeoutId; return function debounced(...args) { clearTimeout(timeoutId); timeoutId = setTimeout(() => { callback.apply(this, args); }, delay); }; }
The timeoutId variable belongs to one returned wrapper. Create separate debounced functions when separate controls should keep separate timers.
const wait = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)); const runSearch = debounce((query) => { console.log(`Searching for: ${query}`); }, 250); runSearch("j"); runSearch("ja"); runSearch("jav"); runSearch("java"); console.log("Queued 4 rapid calls"); await wait(350);
clearTimeout() is safe on the first call because there is no matching timer to cancel yet. After that, each new call cancels the previous pending callback.
$ node debounce-demo.mjs Queued 4 rapid calls Searching for: java
const searchInput = document.querySelector("#search"); const updateResults = debounce((event) => { fetchResults(event.target.value); }, 300); searchInput.addEventListener("input", updateResults);
The input event fires for user edits in fields such as input and textarea, so the debounce delay prevents one search request per keystroke.
$ rm debounce-demo.mjs