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:
1000means 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.
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.
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.
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.
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
Cancel the other one
In the first example, cancel
firstinstead ofcancelled. Predict the three lines the console prints now. - 2
Forget clearInterval
In the countdown, comment out the
clearInterval(id);line and thereturn;. Predict what happens afterLift off!, then run it and watch the number. - 3
Grow the backoff
In the third example, change
attempt < 3toattempt < 5. Predict the waits array before running. (The example'swaitis 800 ms, so raise it to2000too.)
Code & diagrams
Timer B was cancelled before its 50 ms passed. The extra arguments `"Asha"` and `3` were handed to C's callback.
Preview
Console
Nothing logged yet.Expected console output
Timer ids are numbers
A: after 100 ms
C: hello Asha 3Without `clearInterval`, the callback would keep running every 200 ms and count into negative numbers.
Preview
Console
Nothing logged yet.Expected console output
3
2
1
Lift off!Each run decides when the next one happens, which `setInterval` can't do. Real retry code (Phase 11) uses this pattern.
Preview
Console
Nothing logged yet.Expected console output
Attempt 1
Attempt 2
Attempt 3
Gave up. Waits used: [ 50, 100 ]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
Console
Nothing logged yet.Expected console output
Levels: 10
Took at least 15 ms (clamping kicked in)? trueBreak 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);.
Break #2
Lose the interval id
Start setInterval(update, 1000) inside a click handler without storing the id, then click the button three times.
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
Timeoutobject, 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.requestAnimationFrameruns right before each repaint (Topic 11.7). - ▸
If you pass a string instead of a function,
setTimeout("doIt()", 100), the browser runs it likeeval. 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;visibilitychangeevents let you pause and resume cleanly.
Remember this
- 1
setTimeout(callback, delay)asks the browser to callcallbackonce, after at leastdelaymilliseconds (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 callsgreet("Asha"). If you leave out the delay, it is0. - 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
setInterval(callback, delay)callscallbackrepeatedly, roughly everydelayms, until you callclearInterval(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
Pass the function itself, not the result of calling it.
setTimeout(save, 1000)is right.setTimeout(save(), 1000)callssaveimmediately and hands its return value (usuallyundefined) to the timer, so nothing happens later. To pass arguments, wrap the call in an arrow function:setTimeout(() => save("draft"), 1000). - 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
A **repeating
setTimeout** (the callback schedules the next one at its end) is often better thansetInterval. It guarantees a full gap after each run finishes, and you can change the delay each time (for example, wait longer after an error).setIntervalkeeps a fixed rhythm instead, regardless of how long each run takes. Delays larger than2147483647ms (about 24.8 days) overflow and fire immediately.
Explain it without notes
What is the difference between setTimeout and setInterval, and how do you stop each?
Why can a timer fire later than its delay? Give at least three reasons.
When would you use a repeating setTimeout instead of setInterval?
What goes wrong with setTimeout(save(), 500)?
Practice
Write code that logs tick 1, tick 2, tick 3, 100 ms apart, using setInterval, then stops.
Write delayLog(message, ms) that logs the message after ms, and returns a function that cancels it. Show it cancelling one message.
Trade-offs
- ↔
setIntervalis short and keeps a steady rhythm, but runs can bunch up after delays and it can't adapt; a repeatingsetTimeoutadapts 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
setTimeoutand cancel it withclearTimeout.I can repeat code with
setIntervaland always stop it withclearInterval.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
setIntervaland a repeatingsetTimeout.