Command Palette

Search for a command to run...

Hectal
PHASE 10Intermediate ~20 min· topic 2 of 8

Topic 10.2

Timers

In one line

setTimeout runs a function once after a delay and setInterval runs it again and again; both return an id you pass to clearTimeout or clearInterval to cancel, and every delay is a minimum, never an exact time.

Think of it like this

A kitchen timer and an alarm clock. You set a kitchen timer for 10 minutes and go do something else; when it rings, you take the cake out. That's setTimeout: one ring, once. An alarm that rings every morning until you switch it off is setInterval. And if you're in the shower when it rings, you hear it a bit late: timers can be late, but never early.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Millisecond (ms)
One thousandth of a second. Timer delays are written in ms: 1000 means one second.
setTimeout
Runs a function once, after at least the given delay.
setInterval
Runs a function again and again, about every given delay, until it's cleared.
Timer id
The number a timer function returns, used to cancel that timer.
clearTimeout / clearInterval
Cancel a timer using its id, so its callback won't run (again).
Clamping
The browser raising a too-small delay to a minimum, such as 4 ms for deeply nested timers.
Throttling (timers)
The browser running timers less often, for example in background tabs, to save battery.

Step by step

01Run something once, later

Give setTimeout a function and a delay. The function runs once after at least that delay. Your code carries on straight away.

once.jswhole filejavascript
setTimeout(() => {
  console.log("Two seconds passed");
}, 2000);
console.log("Timer started");

02Keep the id to cancel it

A "Saved!" message should hide after 3 seconds, but if the user saves again, the 3 seconds should restart. Clear the old timer and start a new one.

toast.jswhole filejavascript
let hideTimer;
function showSaved() {
  toast.hidden = false;
  clearTimeout(hideTimer);            // safe even the first time
  hideTimer = setTimeout(() => {
    toast.hidden = true;
  }, 3000);
}

03Repeat with setInterval, and stop it

A clock updates every second. Store the id so a Stop button (or leaving the page section) can end it.

clock.jswhole filejavascript
const clockId = setInterval(() => {
  clock.textContent = new Date().toLocaleTimeString();
}, 1000);

stopButton.addEventListener("click", () => clearInterval(clockId));

04setInterval vs repeating setTimeout

setInterval aims for a fixed rhythm. A repeating setTimeout waits the full delay after each run. If a run takes 300 ms and the delay is 1000 ms, the interval starts every 1000 ms, while the repeating timeout starts every 1300 ms.

setInterval vs repeating setTimeoutdiagram
Rendering diagram…

05Know when timers are late

Busy main thread, nesting deeper than 5 levels (4 ms minimum), background tab (about 1 second minimum), device sleep: all make timers fire later. For anything that must track real time, like a countdown, read the actual clock (Date.now() or performance.now()) on each tick instead of counting ticks.

Try it yourself

  1. 1

    Cancel the other one

    In the first example, cancel first instead of cancelled. Predict the three lines the console prints now.

  2. 2

    Forget clearInterval

    In the countdown, comment out the clearInterval(id); line and the return;. Predict what happens after Lift off!, then run it and watch the number.

  3. 3

    Grow the backoff

    In the third example, change attempt < 3 to attempt < 5. Predict the waits array before running. (The example's wait is 800 ms, so raise it to 2000 too.)

Code & diagrams

setTimeout, extra arguments and clearTimeout New tab

Timer B was cancelled before its 50 ms passed. The extra arguments `"Asha"` and `3` were handed to C's callback.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

Timer ids are numbers
A: after 100 ms
C: hello Asha 3
A countdown with setInterval that stops itself New tab

Without `clearInterval`, the callback would keep running every 200 ms and count into negative numbers.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

3
2
1
Lift off!
A repeating setTimeout with a changing delay New tab

Each run decides when the next one happens, which `setInterval` can't do. Real retry code (Phase 11) uses this pattern.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

Attempt 1
Attempt 2
Attempt 3
Gave up. Waits used: [ 50, 100 ]
Nested timers are clamped to 4 ms New tab

After 5 levels of nesting, each 0 ms delay becomes 4 ms. That's why chained `setTimeout(fn, 0)` is a poor way to schedule many small jobs.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

Levels: 10
Took at least 15 ms (clamping kicked in)? true

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Call the function instead of passing it

Write setTimeout(console.log("Hi"), 2000);.

terminal
$ Console
── what you'll see ──
Hi (printed immediately, and nothing happens after 2 seconds)

Break #2

Lose the interval id

Start setInterval(update, 1000) inside a click handler without storing the id, then click the button three times.

terminal
$ the page shows
── what you'll see ──
The display updates three times per second, and nothing can stop it.

Myth vs fact

Myth

setTimeout(fn, 1000) runs fn exactly one second later.

Fact

It runs at least one second later: only once the timer fires and the main thread is free. Busy code, nesting, background tabs and sleep all make it later.

Myth

setTimeout(fn, 0) runs fn immediately.

Fact

It queues fn as a new task. All the remaining synchronous code and every pending promise callback (microtask) run first.

Myth

clearTimeout and clearInterval only work on their own kind of timer.

Fact

In browsers they share one list of ids, so either works on either. Use the matching one anyway, for readers.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Timers come from the HTML standard (the browser host), not ECMAScript. In Node.js they return a Timeout object, not a number, and have extra methods (unref() lets the process exit while the timer is pending). Code that stores timer ids should not assume a number.

  • ▸

    For animation, don't use setInterval(draw, 16): it drifts and isn't synced to the screen. requestAnimationFrame runs right before each repaint (Topic 11.7).

  • ▸

    If you pass a string instead of a function, setTimeout("doIt()", 100), the browser runs it like eval. It's slow, uses global scope and is blocked by a strict Content Security Policy. Always pass a function.

  • ▸

    For accurate countdowns, compute the remaining time from a fixed end time on each tick (end - Date.now()), so late ticks don't add up. Chrome's "intensive throttling" limits chained timers in pages hidden for 5 minutes to once a minute; visibilitychange events let you pause and resume cleanly.

Remember this

  1. 1

    setTimeout(callback, delay) asks the browser to call callback once, after at least delay milliseconds (1000 ms = 1 second). It returns at once with a timer id, a positive whole number that names this timer. Extra arguments after the delay are passed to the callback: setTimeout(greet, 500, "Asha") later calls greet("Asha"). If you leave out the delay, it is 0.

  2. 2

    clearTimeout(id) cancels a timer that hasn't fired yet. Clearing a timer that already fired, or an id that doesn't exist, does nothing and causes no error. That's why a common pattern is "clear the old timer, then start a new one" (you used it for debouncing in Topic 9.7).

  3. 3

    setInterval(callback, delay) calls callback repeatedly, roughly every delay ms, until you call clearInterval(id). Always keep the id and clear the interval when you're done (a countdown reaches zero, the user leaves the screen). An interval nobody clears keeps running, using CPU and memory, for as long as the page is open.

  4. 4

    Pass the function itself, not the result of calling it. setTimeout(save, 1000) is right. setTimeout(save(), 1000) calls save immediately and hands its return value (usually undefined) to the timer, so nothing happens later. To pass arguments, wrap the call in an arrow function: setTimeout(() => save("draft"), 1000).

  5. 5

    Why delays are minimums: when the time is up, the browser only queues the callback. It runs when the main thread is free (Topic 10.3). On top of that, browsers add rules called clamping: once timers are nested more than 5 levels deep (a timer that sets a timer that sets a timer…), each delay below 4 ms becomes 4 ms. In a background tab, timers usually fire at most once per second, and Chrome may hold long-hidden pages' chained timers to once per minute to save battery.

  6. 6

    A **repeating setTimeout** (the callback schedules the next one at its end) is often better than setInterval. It guarantees a full gap after each run finishes, and you can change the delay each time (for example, wait longer after an error). setInterval keeps a fixed rhythm instead, regardless of how long each run takes. Delays larger than 2147483647 ms (about 24.8 days) overflow and fire immediately.

Explain it without notes

01

What is the difference between setTimeout and setInterval, and how do you stop each?

02

Why can a timer fire later than its delay? Give at least three reasons.

03

When would you use a repeating setTimeout instead of setInterval?

04

What goes wrong with setTimeout(save(), 500)?

Practice

01

Write code that logs tick 1, tick 2, tick 3, 100 ms apart, using setInterval, then stops.

02

Write delayLog(message, ms) that logs the message after ms, and returns a function that cancels it. Show it cancelling one message.

Trade-offs

  • ↔

    setInterval is short and keeps a steady rhythm, but runs can bunch up after delays and it can't adapt; a repeating setTimeout adapts and never overlaps, but needs a little more code.

  • ↔

    Short polling intervals make updates feel instant but waste battery and network; longer ones are cheaper but slower to show changes. Server push (WebSockets, server-sent events) avoids polling altogether.

Done when you can

  • I can run code once later with setTimeout and cancel it with clearTimeout.

  • I can repeat code with setInterval and always stop it with clearInterval.

  • I pass a function to timers, never the result of calling one.

  • I can list reasons a timer fires late, including clamping and background tabs.

  • I can choose between setInterval and a repeating setTimeout.