Home/Learn/JavaScript & TypeScript/Timers, Debounce and Throttle

Timers, Debounce and Throttle

Intermediate
Async

setTimeout and setInterval schedule work; debounce and throttle limit how often it happens. Both patterns are closures, and both need cleanup.

Overview

Timers are the simplest async primitive and the easiest to leak. setInterval keeps firing until cleared, so a component that starts one and unmounts without clearing leaves a callback running against dead state forever. Debounce and throttle are the two rate-limiting patterns you will implement or import constantly: debounce waits for a pause and is right for search-as-you-type, throttle enforces a maximum rate and is right for scroll and resize.

Timers and Cleanup

The delay is a minimum, not a guarantee — the callback runs when the stack is clear and its turn arrives.

setInterval leaks without cleanup
const id = setTimeout(fn, 1000)
clearTimeout(id)

const iid = setInterval(fn, 1000)
clearInterval(iid)              // otherwise it runs forever

// The leak, in React:
useEffect(() => {
  const id = setInterval(tick, 1000)
  return () => clearInterval(id)      // without this, every mount adds a timer
}, [])

// The delay is a floor. This does not print after exactly 100ms:
setTimeout(() => console.log('later'), 100)
blockFor(500)      // the timer waits for the stack to clear

// Nested timers are clamped to >= 4ms after 5 levels of nesting.

Debounce — Wait for the Pause

Runs once, after the caller stops calling. The right choice for search inputs and autosave.

Debounce: one call after the last one
function debounce(fn, ms = 300) {
  let timer
  return (...args) => {
    clearTimeout(timer)
    timer = setTimeout(() => fn(...args), ms)
  }
}

// Typing "react" fires one request, not five
const search = debounce(q => fetchResults(q), 300)
input.addEventListener('input', e => search(e.target.value))

// With cancellation, which matters on unmount:
function debounce(fn, ms = 300) {
  let timer
  const wrapped = (...args) => {
    clearTimeout(timer)
    timer = setTimeout(() => fn(...args), ms)
  }
  wrapped.cancel = () => clearTimeout(timer)
  return wrapped
}

Throttle — At Most Once Per Interval

Runs at a steady maximum rate while calls keep coming. The right choice for scroll, resize and mousemove.

Throttle, and when to use rAF instead
function throttle(fn, ms = 200) {
  let last = 0
  return (...args) => {
    const now = Date.now()
    if (now - last >= ms) {
      last = now
      fn(...args)
    }
  }
}

window.addEventListener('scroll', throttle(updateHeader, 200))

// Choosing between them:
//   debounce -> "tell me when they have finished"   (search, autosave, resize-end)
//   throttle -> "tell me regularly while it happens" (scroll position, drag)

// For animation, neither: use requestAnimationFrame, which syncs to paint
const onScroll = () => requestAnimationFrame(updateParallax)

Key Points to Remember

  • 1A timer delay is a minimum — the callback runs once the stack is clear, not at an exact time
  • 2setInterval runs until cleared; not clearing it on unmount is a real and common leak
  • 3Debounce fires once after calls stop — right for search-as-you-type and autosave
  • 4Throttle fires at most once per interval while calls continue — right for scroll and resize
  • 5For visual updates use requestAnimationFrame instead, since it synchronises with the browser paint

Interview Questions

Sign in to ask Aria
1

What is the difference between debounce and throttle, and when would you use each?

Medium
2

Implement throttle from scratch.

Medium
3

Why is setTimeout(fn, 100) not guaranteed to run after exactly 100ms?

Medium

Ask Aria about Timers, Debounce and Throttle

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…