JavaScript Timers Table

FunctionWhat it doesField note
setTimeout(fn, ms)Run once after msRuns AFTER the timer - never before 4ms nested clamp
setInterval(fn, ms)Run every msOverlapping runs if fn is slower than ms - self-scheduling beats it
clearTimeout / clearIntervalCancel before it firesIDs are shared pools in browsers - keep the handle
requestAnimationFrame(fn)Run before next paint~60fps sync; the ONLY right way to animate DOM
queueMicrotask(fn)Run after this task, before renderMicrotasks run before the next macrotask
this in callbacksundefined in strict modeArrow functions keep this; function() loses it
setTimeout(fn, 0)Next macrotask, not instantlyStill yields to rendering and microtasks
Reference: the MDN setTimeout reference. Three truths prevent most timer bugs: timers guarantee a MINIMUM delay, never exact timing (a busy tab delays callbacks); nested setTimeout past 5 levels gets clamped to 4ms minimum by browsers; and setInterval can stack executions when fn runs longer than the interval - the self-scheduling pattern (fn ends by calling setTimeout(fn, ms)) never overlaps. Bottom line: requestAnimationFrame for visuals, setTimeout for everything delayed, and never trust a 0 delay to run immediately. Related tools: closures table (timers close over state), promise table for the async layer, and DOM traversal table for what callbacks update.

JavaScript timers schedule work onto the event loop: setTimeout runs once after a delay, setInterval repeats, requestAnimationFrame syncs to the display, queueMicrotask jumps the queue. The table below is the working seven, with the timing truths the spec actually guarantees - minimums, clamps and queue order.

Bottom line: timers guarantee a MINIMUM delay, never exact timing. A busy tab delays callbacks, background tabs throttle to once per second, and nested setTimeout past five levels clamps to 4ms. The code that survives this reality: requestAnimationFrame for visuals, self-scheduling setTimeout for polling, and queueMicrotask only when ordering against promises matters.

The honest part: the overlapping-interval trap is the deepest one. setInterval fires every ms regardless of whether the previous callback finished - a slow callback stacks executions. The fix is self-scheduling: each run ends by calling setTimeout(fn, ms) for itself, guaranteeing a gap between the END of one run and the START of the next. It reads unusual; it is the only version that cannot overlap.

How to use

  1. Pick by timing model: run-once-later (setTimeout), repeat-on-schedule (setInterval, with care), per-frame (requestAnimationFrame).
  2. Keep the handle: the number each timer returns is what clearTimeout and clearInterval cancel - and the pools are separate, so cancel with the matching clear.
  3. Test timing assumptions: a 0ms setTimeout still waits for the current task and rendering - it is next tick, not now.

Frequently asked questions

Why does setTimeout(fn, 0) not run immediately?

Because timers are macrotasks: the engine finishes the current code, runs all microtasks (promises, queueMicrotask), renders if due, and only then picks the next macrotask. A 0ms timeout means 'as soon as the queue allows', which after heavy work can be hundreds of milliseconds. Browsers also clamp nested timeouts: the fifth nested level onward enforces a 4ms minimum. For run-soonest scheduling, queueMicrotask or Promise.resolve().then(fn) runs before the next macrotask.

Why can setInterval stack up and overlap?

Because setInterval fires on schedule regardless of whether the previous callback finished - if fn takes 300ms and the interval is 100ms, executions queue and then run back-to-back. The self-scheduling pattern fixes it: function poll() { doWork(); setTimeout(poll, 100); } guarantees a full 100ms gap between the END of one run and the START of the next. Same cadence, zero overlap.

When should I use requestAnimationFrame instead of setTimeout?

Whenever the callback changes what the user sees: rAF fires before the next paint, synced to the display refresh (~60fps), so animations never tear or waste frames in hidden tabs (rAF pauses there; timers do not). setTimeout(fn, 16) approximates it but drifts against the actual frame, and keeps firing in background tabs. The rule: paint-synced work is rAF's job; delayed work is setTimeout's.

How does this behave inside a timer callback?

In strict mode it is undefined - the callback is called without a receiver, and function declarations lose the surrounding this. Arrow functions capture this lexically from where they were created, which is why arrow callbacks inside methods see the right object. The legacy fix was const self = this before the setTimeout; the modern fix is an arrow function, and the difference matters the moment the callback touches this.state or this.props.

Related tools