JavaScript Timers Table
| Function | What it does | Field note |
|---|---|---|
setTimeout(fn, ms) | Run once after ms | Runs AFTER the timer - never before 4ms nested clamp |
setInterval(fn, ms) | Run every ms | Overlapping runs if fn is slower than ms - self-scheduling beats it |
clearTimeout / clearInterval | Cancel before it fires | IDs 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 render | Microtasks run before the next macrotask |
this in callbacks | undefined in strict mode | Arrow functions keep this; function() loses it |
setTimeout(fn, 0) | Next macrotask, not instantly | Still yields to rendering and microtasks |
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
- Pick by timing model: run-once-later (setTimeout), repeat-on-schedule (setInterval, with care), per-frame (requestAnimationFrame).
- 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.
- 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.