Technology · JavaScript

Intermediate

JavaScript Timers

Schedule code with setTimeout and setInterval, then rate-limit handlers with debounce and throttle.

TL;DR
  1. 01Schedule delayed code execution with setTimeout and setInterval.
  2. 02Cancel pending timers using clearTimeout and clearInterval methods.
  3. 03Rate-limit frequent events with debounce and throttle patterns.

setTimeout and clearTimeout

  • Use setTimeout to run a function once after a delay, measured in milliseconds.
    setTimeout(() => {
      console.log('Runs after 1 second');
    }, 1000);
    
  • setTimeout returns a timer ID you can use to cancel it later.
    const timerId = setTimeout(() => console.log('delayed'), 5000);
    
  • Use clearTimeout with that ID to cancel the callback before it fires.
    clearTimeout(timerId); // "delayed" never logs
    
  • Pass extra arguments to setTimeout after the delay, and they forward to the callback.
    setTimeout((name) => console.log(`Hi, ${name}`), 1000, 'Ana');
    
  • A delay of 0 still queues the callback after current synchronous code finishes.
    console.log('first');
    setTimeout(() => console.log('third'), 0);
    console.log('second');
    

setInterval and clearInterval

  • Use setInterval to repeatedly run a function on a fixed time interval.
    const id = setInterval(() => {
      console.log('tick');
    }, 1000);
    
  • Always store the returned ID so the interval can be stopped later.
    let count = 0;
    const id = setInterval(() => {
      count++;
      if (count >= 5) clearInterval(id);
    }, 1000);
    
  • Use clearInterval to stop a repeating timer and prevent memory leaks.
    clearInterval(id); // stops all future ticks
    
  • Clear intervals when a component unmounts or a feature is no longer needed.
    function startPolling() {
      const id = setInterval(fetchUpdates, 5000);
      return () => clearInterval(id); // cleanup function
    }
    
  • Prefer recursive setTimeout over setInterval when callback duration might exceed the interval.
    function poll() {
      doWork();
      setTimeout(poll, 1000); // waits for doWork to finish first
    }
    poll();
    

Timer Precision and the Event Loop

  • JavaScript runs on a single thread, so timers wait for the call stack to clear before firing.
    setTimeout(() => console.log('fires late'), 0);
    while (Date.now() < Date.now() + 200) {} // blocks the thread
    
  • The delay you pass is a minimum wait, not an exact schedule.
    // Browsers often clamp nested timeouts to a 4ms minimum after several levels of nesting
    
  • Long-running synchronous code delays every pending timer callback, not just one.
    setTimeout(() => console.log('A'), 10);
    setTimeout(() => console.log('B'), 20);
    // Both wait if the main thread is busy with heavy computation
    
  • Timers fire as macrotasks, running after the current synchronous code and any pending microtasks like promises.
    console.log('1');
    setTimeout(() => console.log('3'), 0);
    Promise.resolve().then(() => console.log('2'));
    
  • Background browser tabs often throttle timers to save battery, increasing delays further.
    // setInterval may slow to roughly once per second in inactive tabs
    

Debounce and Throttle Patterns

  • Debounce delays a function until a pause occurs, resetting the timer on each new call.
    function debounce(fn, delay) {
      let timerId;
      return (...args) => {
        clearTimeout(timerId);
        timerId = setTimeout(() => fn(...args), delay);
      };
    }
    
  • Use debounce on search inputs so the API call only fires after typing stops.
    const handleSearch = debounce((query) => fetchResults(query), 300);
    input.addEventListener('input', (e) => handleSearch(e.target.value));
    
  • Throttle limits a function to run at most once per fixed time window.
    function throttle(fn, limit) {
      let waiting = false;
      return (...args) => {
        if (waiting) return;
        fn(...args);
        waiting = true;
        setTimeout(() => (waiting = false), limit);
      };
    }
    
  • Use throttle on scroll or resize handlers to limit how often expensive work runs.
    const handleScroll = throttle(() => updateScrollPosition(), 200);
    window.addEventListener('scroll', handleScroll);
    
  • Choose debounce when only the final event matters, and throttle when steady updates matter.
    // Debounce: save draft after user stops typing
    // Throttle: update a progress bar while the user keeps scrolling
    

requestAnimationFrame for Visual Updates

  • Use requestAnimationFrame to schedule a callback right before the browser's next repaint.
    function animate() {
      moveElement();
      requestAnimationFrame(animate);
    }
    requestAnimationFrame(animate);
    
  • This produces smoother motion than setInterval because it syncs with the display's refresh rate.
    // Typically runs about 60 times per second, matching most monitors
    
  • Use cancelAnimationFrame with the returned ID to stop an animation loop.
    const id = requestAnimationFrame(animate);
    cancelAnimationFrame(id);
    
  • Browsers automatically pause requestAnimationFrame callbacks in inactive or hidden tabs.
    // Saves battery and CPU compared to a setInterval-based animation loop
    
  • Prefer requestAnimationFrame over throttled scroll handlers for DOM updates tied to scrolling.
    let ticking = false;
    window.addEventListener('scroll', () => {
      if (!ticking) {
        requestAnimationFrame(() => {
          updateParallax();
          ticking = false;
        });
        ticking = true;
      }
    });
    
Tips
  1. 01Always store the timer ID returned by setTimeout or setInterval so you can cancel it later if conditions change.
  2. 02Use debounce for input or search fields, and throttle for scroll or resize handlers that fire continuously.
  3. 03Prefer requestAnimationFrame over setInterval for visual updates, since it syncs with the browser's actual repaint cycle.
Warnings
  1. 01Timer delays are minimums, not guarantees, because the single-threaded event loop can delay callbacks under heavy load.
  2. 02Forgetting to clear an interval causes it to keep running forever, often leaking memory in long-lived applications.
  3. 03Nesting setTimeout calls recursively without clearing the previous one can silently stack up duplicate timers.
FAQ
JavaScript SymbolsJavaScript Type Coercion