Skip to content
WANCODE
BM

Debounce Function

Wait until the user stops typing (or resizing, or scrolling) before doing expensive work — with a cancel method.

Live preview Open in Playground
debounce.js
/**
 * Returns a version of `fn` that only runs after the calls
 * have stopped for `wait` milliseconds.
 */
export function debounce(fn, wait = 300) {
  let timeoutId;

  function debounced(...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => {
      timeoutId = undefined;
      fn.apply(this, args);
    }, wait);
  }

  // Let callers throw away a pending call (e.g. when a component unmounts)
  debounced.cancel = () => {
    clearTimeout(timeoutId);
    timeoutId = undefined;
  };

  return debounced;
}

An input event fires on every keystroke. If each one triggers a search, typing "kuala lumpur" sends 12 searches. Debouncing says: restart a timer on every call, and only run when the timer finishes. Typing quickly keeps restarting the timer, so the function runs once — after the pause.

Line by line

  • let timeoutId lives in a closure: every call to debounced sees the same variable, even though debounce itself finished running long ago.
  • clearTimeout(timeoutId) cancels the previous pending run. Calling it with undefined is safe — it simply does nothing.
  • ...args collects whatever arguments were passed (like the event), and fn.apply(this, args) passes them on with the same this. That's why debounced is a normal function, not an arrow function — arrows don't get their own this.
  • debounced.cancel is a property on the function (functions are objects in JavaScript). Call it when the work is no longer wanted.

Debounce vs throttle

Debounce runs once after activity stops (search boxes, auto-save). Throttle runs at most once every N ms during activity (scroll position, drag). Different tools for different jobs.

Usage

JavaScript
import { debounce } from "./debounce.js";

const search = document.querySelector("#search");

const runSearch = debounce((event) => {
  console.log("Searching for:", event.target.value);
}, 400);

search.addEventListener("input", runSearch);

// Later, if the search box is removed:
// runSearch.cancel();