Timers: setTimeout & setInterval
Schedule JavaScript callbacks with setTimeout and setInterval, cancel them safely, compare repeating patterns, and measure why timer delays are minimums rather than promises.
- 01Schedule and cancelUse
setTimeout,setInterval,clearTimeout, andclearIntervalsafely. - 02Choose the repeating patternPick
setIntervalor nestedsetTimeoutbased on overlap, drift, and cleanup. - 03Measure timer honestyExplain clamping, throttling, overflow, and why a delay is only a minimum.
Timers are reminders, not pauses
JavaScript runs one piece of code until that piece is finished. A timer does not freeze the program in place. Instead, a host such as the browser or Node.js keeps a reminder and calls your callback later. That fits the previous lesson’s idea: the host does the waiting, and the callback runs after the current JavaScript has finished.
In this lesson, setTimeout schedules one future callback, setInterval schedules repeated callbacks, and the matching clear... functions cancel work you no longer want. You will measure real delays in your own tab because timing is famously environment-dependent: device speed, tab visibility, battery saving, and page load all matter.
Think of setTimeout as setting a kitchen timer. You keep cooking now. Later, the timer rings once. If you decide the dish is cancelled, clearTimeout is turning that timer off before it rings.
- In real life: Set a timer for five minutes
- In JavaScript:
setTimeout(callback, 5000) - In real life: Turn the timer off before it rings
- In JavaScript:
clearTimeout(id) - In real life: The chef is busy plating food when it dings
- In JavaScript: The callback waits until JavaScript is free
Where the analogy stops: A kitchen timer rings by itself. JavaScript callbacks still need the main thread to be available, so the ding can wait behind busy code.
A JavaScript timer asks the host to call a function after at least a delay; it never promises the callback will run at the exact millisecond.
setTimeout & clearTimeout
STEP THROUGHsetTimeout(callback, delay) returns immediately. In browsers it returns a numeric id; in Node.js it returns a Timeout object. Either way, keep that value if you might cancel the timer. The optional values after the delay are passed to the callback as arguments when it runs.
setTimeout((name, count) => {
console.log(name + " has " + count + " reminders");
}, 1000, "Ada", 3);The source below schedules two timeouts, cancels one, and logs synchronously before the callbacks get a chance. Step through the real order: a timeout with 0 still runs later, not in the middle of the current script.
Predict the order. The timeouts are real browser-style timers, but this player replays the recorded order so you can step through it.
script
const keep = setTimeout(() => console.log("D: 20ms timeout"), 20);const cancel = setTimeout(() => console.log("never runs"), 10);setTimeout(() => console.log("C: 0ms timeout"), 0);clearTimeout(cancel);console.log("B: end");Cancellation is most important during cleanup. If a component resets, unmounts, or navigates away, clear any pending timer so an old callback does not update stale UI.
const started = performance.now();const id = setTimeout(() => { const actual = performance.now() - started; console.log(actual);}, 500); // Optional: block the thread before the callback can run.Run the lab, then try adding busy work. The measurement uses performance.now() in your browser when you click.
setInterval vs nested setTimeout
INTERACTIVEsetInterval(fn, 500) is a metronome: it keeps trying to tick every 500 ms. That is fine for a clock or simple repeated check. But if the work sometimes takes longer than expected, interval callbacks can bunch up or overlap with async work you started inside them.
An interval is like a metronome. It keeps ticking even if you fall behind. A nested timeout is more like resetting a kitchen timer each time you finish a task, so there is always a full gap after the work.
- In real life: A metronome keeps ticking even if you miss a beat
- In JavaScript:
setIntervalkeeps scheduling starts - In real life: Finish chopping, then set the next reminder
- In JavaScript: Nested
setTimeoutwaits until work is done - In real life: Press Stop on the metronome
- In JavaScript:
clearInterval(id)orclearTimeout(id)in cleanup
Where the analogy stops: Neither pattern gives hard real-time guarantees. For animation use requestAnimationFrame; for audio use audio-specific scheduling.
setInterval(async () => {
await saveSlowly();
}, 500);
async function loop() {
await saveSlowly();
timeoutId = setTimeout(loop, 500);
}setInterval(tick, 500); function nestedTick() { doSlowWork(150); setTimeout(nestedTick, 500);}No ticks yet.
Start the comparison. It runs for about five seconds, simulates 150 ms of work on every tick, and cleans up all timers on Stop, Reset, or unmount.
The right choice is practical: choose setInterval for regular lightweight ticks; choose nested setTimeout when each run should finish before the next delay starts, or when the next delay changes after every attempt.
- Hide a toast once, 4 seconds after it appears
- Update a simple clock display every second
- Ask a server again only after the previous request finished
- Save 500 ms after the user stops typing
- Tick a training metronome until Stop is pressed
- Retry with a different delay after each failed attempt
Sort each job by the timer pattern you would reach for first.
Minimum delays & clamping
REAL MEASUREMENTSA delay of 0 means “as soon as the host is allowed to run this callback,” not “right now.” Browsers also protect the page and the device by enforcing minimum delays in some situations. The HTML spec clamps nested timers to at least 4 ms after five levels of nesting. Your measured numbers may be larger or noisier.
let depth = 0;let last = performance.now(); function next() { const now = performance.now(); console.log(depth, now - last); last = now; depth += 1; if (depth < 10) setTimeout(next, 0);} setTimeout(next, 0);No measurements yet.
Run ten nested 0 ms timeouts. You are measuring minimum-delay behavior, not guaranteed exact scheduling.
Timer delays are handled with 32-bit-ish limits. In Node.js, setTimeout(fn, 2 ** 31) fires almost immediately and emits a TimeoutOverflowWarning. Browsers also apply 32-bit timer handling, so very large delays are not a reliable way to wait for weeks.
Trust issues with setTimeout
MEASUREThe trustworthy sentence is: a timeout waits at least the requested delay. It can be later because the current script is busy, another callback is running, the browser throttles a background tab, or the device is under load. Background browser tabs are commonly throttled to about once per second, and some browsers become more aggressive in certain cases.
This is why every timing demo in this lesson measures with performance.now() after you click. The displayed results are not copied from the author’s machine; they are measured on this device, in this tab, now.
| API | What it schedules | Cancel with | Return value | Notes |
|---|---|---|---|---|
setTimeout(fn, delay, ...args) | Runs once after at least delay ms | clearTimeout(id) | Browser: number. Node.js: Timeout object. | Extra arguments are passed to fn when it runs. |
setInterval(fn, delay, ...args) | Runs repeatedly, trying to start every delay ms | clearInterval(id) | Browser: number. Node.js: Timeout object. | Stop it yourself or it keeps scheduling callbacks. |
Nested setTimeout | Runs once, then schedules the next run after work finishes | Store the latest id and call clearTimeout(id) | Same as setTimeout | Great for polling or avoiding overlap. |
Node setImmediate(fn) | Node-specific: run after the current poll phase | clearImmediate(id) | Node Immediate object | Mentioned for Node differences; browsers do not have it. |
Where you’ll use timers
Timers show up in user interfaces constantly: hiding a toast, debouncing search input, retrying a request, showing a countdown, and stopping a spinner if something takes too long. Two published lessons connect closely: Closures in loops, timers & memory explains the classic loop variable problem, and Losing this explains why setTimeout(obj.method) can detach a method from its object.
let timeoutId;
let stopped = false;
async function poll() {
await loadLatestData();
if (!stopped) timeoutId = setTimeout(poll, 1000);
}
poll();
function stopPolling() {
stopped = true;
clearTimeout(timeoutId);
}Notice the cleanup story: stopped prevents scheduling another timeout, and clearTimeout(timeoutId) cancels a waiting one. Real apps put that cleanup in a Reset button, a component unmount effect, or a route-change handler.
Common misconceptions
- “0 ms means immediately.” It means later, after the current script and only when the host can run the callback.
- “500 ms means exactly 500 ms.” It means at least 500 ms; busy code and throttling can delay it.
- “Intervals wait for async work.” They do not know whether your promise finished. Use nested timeouts or explicit guards.
- “Clearing after a timeout fires changes history.” Clearing only cancels a pending timer. Once a callback ran, it ran.
- “Huge delays are safe reminders.” Values above
2 ** 31 - 1ms overflow in Node and are unsafe in browsers too. - “Timer ids are the same everywhere.” Browsers use numbers; Node returns objects.
Practice exercises
5 EXERCISESRead the code and type the four printed words in order.
console.log("start");
setTimeout(() => console.log("slow"), 20);
setTimeout(() => console.log("fast"), 0);
console.log("end");The script prints start, schedules two timers, prints end, then the 0 ms callback prints fast, then the 20 ms callback prints slow.
Write a countdown that prints 5, 4, 3, 2, 1, 0 with a nested timeout and has a cancel function.
let timeoutId;
let remaining = 5;
function tick() {
// print remaining, then schedule the next tick
}
function cancelCountdown() {
// stop the pending tick
}let timeoutId;
let remaining = 5;
function tick() {
console.log(remaining);
remaining -= 1;
if (remaining >= 0) {
timeoutId = setTimeout(tick, 1000);
}
}
function cancelCountdown() {
clearTimeout(timeoutId);
}
tick();The nested timeout schedules only one future tick at a time. cancelCountdown clears that pending id, so a Reset button can stop the countdown safely.
Rewrite the interval as a nested timeout so a slow request cannot overlap the next request.
setInterval(async () => {
await loadLatestData();
}, 1000);let timeoutId;
let stopped = false;
async function poll() {
await loadLatestData();
if (!stopped) timeoutId = setTimeout(poll, 1000);
}
poll();
function stopPolling() {
stopped = true;
clearTimeout(timeoutId);
}The fixed version awaits the work first, then schedules the next timeout. A stop flag plus clearTimeout handles cleanup.
Change the preview so it updates only after the user stops typing for 300 ms.
input.addEventListener("input", () => {
preview.textContent = input.value;
});let previewTimer;
input.addEventListener("input", () => {
clearTimeout(previewTimer);
previewTimer = setTimeout(() => {
preview.textContent = input.value;
}, 300);
});Every input event cancels the old pending update and creates a new one. Only the timer that survives 300 ms updates the preview. Debounce & throttle gets a full lesson later.
Predict what prints when Reset-style cleanup clears one timer but leaves another scheduled.
const id = setTimeout(() => console.log("save"), 10);
clearTimeout(id);
setTimeout(() => console.log("done"), 0);Only done prints. clearTimeout(id) cancels the save callback before it has a chance to run.
Check your understanding
7 QUESTIONSQuestion 1 of 7What does the delay argument to
setTimeout(fn, 500)really mean?Choose an answer to see the explanation.
Question 2 of 7What does this print first, second, and third?
Read the code, then predictconsole.log("A"); setTimeout(() => console.log("C"), 0); console.log("B");Choose an answer to see the explanation.
Question 3 of 7Which cancellation pair is correct?
Choose an answer to see the explanation.
Question 4 of 7Why might nested
setTimeoutbe better thansetIntervalfor polling?Choose an answer to see the explanation.
Question 5 of 7What does the HTML timer nesting rule say about many nested
setTimeout(0)calls?Choose an answer to see the explanation.
Question 6 of 7What usually happens to timers in background browser tabs?
Choose an answer to see the explanation.
Question 7 of 7In browsers, what does
setTimeoutreturn?Choose an answer to see the explanation.
Key takeaways
setTimeoutschedules one future callback;clearTimeoutcancels it before it runs.setIntervalrepeats untilclearInterval; nestedsetTimeoutgives you one full gap after work finishes.- Timer delays are minimums. Measure real behavior with
performance.now()when precision matters. - Nested timers are clamped after several levels, background tabs are throttled, and huge delays can overflow.
- Always clean up timers on Reset, unmount, navigation, or Stop.
Final definition: a JavaScript timer is a host-managed reminder that queues a callback later, never an exact pause in the current code.
Up next: The event loop.