mediumJS ES6+#139

Debounce with leading edge and cancel

Prompt

Implement debounce(fn, wait, options):

  1. Trailing by default: fn runs once, wait ms after the last call
  2. { leading: true }: fn fires immediately on the first call, then ignores calls until quiet
  3. The returned function exposes .cancel() to drop any pending invocation
  4. Arguments and this of the last call are forwarded to fn

This is the search-box / resize-handler primitive — write it like you'd ship it.

Solution

function debounce(fn, wait, { leading = false } = {}) {
  let timer = null
  function debounced(...args) {
    const callNow = leading && timer === null
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      timer = null
      if (!leading) fn.apply(this, args)
    }, wait)
    if (callNow) fn.apply(this, args)
  }
  debounced.cancel = () => {
    clearTimeout(timer)
    timer = null
  }
  return debounced
}
Mentor's take

Debounce exists to convert a stream of events into one decision — without it, a search box fires a request per keystroke and the last response to arrive (not the last query typed) wins the race. That's the why an interviewer wants first; the timer mechanics are secondary.

The design decisions that read as senior:

  • The timer variable is the entire state machine. timer === null means "quiet period over" — that one check implements the leading edge. No booleans, no timestamps.
  • .cancel() is not optional polish. In React, a debounced callback that survives unmount calls setState on a dead component. The cleanup story (useEffect return calling .cancel()) is part of the API, which is why lodash and use-debounce both ship it.
  • fn.apply(this, args) forwards the last call's arguments — with a plain fn() the search box debounces correctly but searches for an empty string.

Red flag: confusing debounce with throttle. Debounce = "wait for silence" (search input); throttle = "at most once per interval" (scroll position). Saying "I'd debounce the scroll handler" ships a UI that only updates when the user stops scrolling.

Say it: "Debounce collapses a burst of events into the final one after quiet time; the leading option trades freshness for immediacy, and cancel is what makes it safe to use inside a component lifecycle."