setTimeout, setInterval and their clear functions

The delay is the earliest the callback may run, never the exact moment. Everything awkward about these functions follows from that, plus the fact that setInterval does not wait for your work to finish.

Global functionsHTML standardLive demo
Common call
const id = setTimeout(fn, 1000)
Returns
a timer id for cancelling
Replaces
nothing; it is the primitive
Watch out
the delay is a MINIMUM, and setInterval accumulates drift
setTimeout(callbackcallback — Run after the delay. A STRING is accepted and evaluated, which is an eval in disguise and should never be used.type: Function · required, delaydelay — Milliseconds to wait at minimum. 0 means "as soon as the current task finishes", not immediately. Nested timeouts are clamped to 4ms after five levels.type: number · default: 0, ...args...args — Extra arguments passed to the callback — cleaner than wrapping it in another closure.type: any · default: none)
→ number | object

Demo

Live evaluation
Try:
Inputs
anumberdelay for A (ms)
bnumberdelay for B (ms)
Output
await new Promise(done => { const log = []; setTimeout(() => log.push('A'), 10); setTimeout(() => log.push('B'), 30); setTimeout(() => done(log), Math.max(10, 30) + 5); })
Pending…

Timers fire in order of their delay, and when two delays are equal they fire in the order they were registered — so the tie and the zero case both give A first. Now try delays only one millisecond apart, such as 5 and 4: run it a few times and the order can flip. That is the page's central point made visible — the delay is a minimum, not a schedule, and timers that close together are not reliably ordered.

Parameters

NameTypeRequiredDescription
callbackFunctionyesRun after the delay. A STRING is accepted and evaluated, which is an eval in disguise and should never be used.
delaynumberno (0)Milliseconds to wait at minimum. 0 means "as soon as the current task finishes", not immediately. Nested timeouts are clamped to 4ms after five levels.
...argsanyno (none)Extra arguments passed to the callback — cleaner than wrapping it in another closure.

Return value

number | object — A timer id — a number in browsers, a Timeout object in Node. Pass it to clearTimeout or clearInterval to cancel.

Common patterns

Cancel on cleanup
Always keep the id.
const id = setTimeout(fn, 1000);
return () => clearTimeout(id);
A promise-based sleep
The idiomatic wrapper.
const sleep = ms => new Promise(r => setTimeout(r, ms));
await sleep(500);
Repeat without drift
Chain timeouts instead of using setInterval.
async function loop() {
  await work();
  setTimeout(loop, 1000);
}

Examples

1. Returns an id
typeof setTimeout(() => {}, 0)
Returns
'object' in Node, 'number' in browsers
2. Zero is not immediate
setTimeout(() => console.log("b"), 0); console.log("a");
Returns
'a' then 'b'
3. Microtasks run first
setTimeout(() => console.log("t"), 0); Promise.resolve().then(() => console.log("m"));
Returns
'm' then 't'
4. Extra arguments
setTimeout((a, b) => a + b, 0, 1, 2)
Returns
the callback receives 1 and 2
5. Cancelling
const id = setTimeout(fn, 1000); clearTimeout(id);
Returns
fn never runs
6. A string is evaluated
setTimeout("alert(1)", 0)
Returns
works — and is an eval

Pitfalls

1. setInterval does not wait for your callback
It fires every n milliseconds regardless of whether the previous run finished, so slow async work overlaps and requests pile up. A chained setTimeout waits for the work, which is almost always what you meant by "every second".
Overlaps
setInterval(async () => { await slowFetch(); }, 1000)
concurrent fetches stack up
Chain it
async function loop() {
  await slowFetch();
  setTimeout(loop, 1000);
}
loop();
one at a time
2. The delay is a minimum
A blocked main thread, a background tab, or a busy event loop all push the callback later — browsers throttle timers in hidden tabs to once a minute. Never use a timer to measure elapsed time or to schedule anything that must be punctual.
Assumes accuracy
setTimeout(() => count++, 1000)
drifts, and stalls in a background tab
Read the clock
const started = Date.now();
setTimeout(() => { const real = Date.now() - started; }, 1000);
the actual elapsed time
3. Losing `this` in a method callback
Passing a bound method unbound means this is undefined in strict mode when the timer fires. An arrow function captures the surrounding this and avoids the problem entirely.
this is lost
setTimeout(this.tick, 100)
TypeError inside tick
Arrow it
setTimeout(() => this.tick(), 100)
works
4. Forgetting to clear it
A timer holds its callback, and the callback holds everything it closes over. An interval started in a component that unmounts keeps running forever, keeping that whole scope alive — a leak and a stream of updates to something that no longer exists.
Runs forever
useEffect(() => { setInterval(poll, 1000); }, [])
never stops
Return a cleanup
useEffect(() => {
  const id = setInterval(poll, 1000);
  return () => clearInterval(id);
}, [])
stops on unmount

When to use

Use it
  • Deferring work until after the current task
  • Debouncing and throttling
  • Timeouts, paired with Promise.race
  • Polling, via a chained setTimeout rather than setInterval
Reach for something else
  • Animation → requestAnimationFrame
  • Running right after the current task → queueMicrotask
  • Measuring time → performance.now()
  • Regular async polling → chain timeouts, not setInterval

Notes

Complexity
O(1) to schedule
Return
A timer id; the callback runs as a macrotask, after all pending microtasks
CPython impl
Not V8 — timers are defined by the HTML standard and by Node, not ECMAScript
Memory
The timer retains the callback and its whole closure until it fires or is cleared
Thread-safe
Single-threaded; a blocked thread delays every timer

FAQ

Because it queues a task for after the current one completes, and all pending microtasks run before any task. So a promise callback queued later still runs first. Zero means "soon", not "now".

setTimeout(() => console.log('t'), 0);
Promise.resolve().then(() => console.log('m'));
// m, then t

History

Netscape
setTimeout and setInterval shipped as browser APIs, never part of ECMAScript.
HTML5
Standardised, including the 4ms nesting clamp and background-tab throttling.