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.
- 01Set and cancel timersUse
setTimeoutandsetIntervalto run code later, andclearTimeoutandclearIntervalto cancel it. - 02Pick a way to repeatChoose
setIntervalor a nestedsetTimeout, based on whether the work might take longer than the wait. - 03Know why timers run lateExplain why a delay is only the shortest wait: busy code, the 4 ms minimum, hidden tabs, and delays that are too big.
A timer is a reminder, not a pause
JavaScript runs one piece of code from start to end before it does anything else. A timer doesn't stop that code. Instead, you hand a function to the host. The host is the program that runs your JavaScript, such as the browser or Node.js. The host waits. Later, it calls your function.
A function that you hand over to be called later is a callback. You met callbacks in the previous lesson. The host does the waiting. The callback runs only after the code that is running now has finished.
This lesson uses four functions:
setTimeoutruns a callback once, later.setIntervalruns a callback again and again.clearTimeoutandclearIntervalcancel a timer that you no longer want.
You will also measure real delays in this tab. The numbers change from one computer to another. A slow computer, a hidden tab, battery saving, or a page that is still loading can all make a timer late.
You set a kitchen timer and keep cooking. Later, the timer rings once. If you no longer need it, you turn it off before it rings. setTimeout sets the timer. clearTimeout turns it off.
- In real life: Set a kitchen 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 cook is busy when it rings, so the cook finishes first
- In JavaScript: The callback waits until JavaScript is free
A JavaScript timer asks the host to call a function after at least a delay. It never promises the exact millisecond.
Set and cancel a timer
STEP THROUGHsetTimeout(callback, delay) takes a callback and a delay. The delay is in milliseconds (ms), and 1000 ms is one second. setTimeout sets the timer and gives back an id right away. An id is a value that names this one timer:
- In a browser, the id is a number.
- In Node.js, the id is a
Timeoutobject.
Save the id if you might want to cancel the timer. You can also pass more values after the delay. The host passes them to the callback when it runs.
setTimeout((name, count) => {
console.log(name + " has " + count + " reminders");
}, 1000, "Ada", 3);Line 3 passes "Ada" and 3 after the delay of 1000 ms. One second later, line 1 gets them as name and count. Line 2 prints Ada has 3 reminders.
A repeating timer works the same way: set it, save the id, and cancel it with the id. The callback below counts three ticks and then stops its own interval.
let count = 0;
const id = setInterval(() => {
count += 1;
console.log("tick", count);
if (count === 3) clearInterval(id);
}, 5);Line 2 saves the id in id before the first tick. Every 5 ms, line 3 adds one to count and line 4 prints it. On the third tick, line 5 passes the same id to clearInterval, so there is no fourth tick.
The next code sets three timers, cancels one, and prints two lines of its own. Step through it and watch when each line prints. A timer with a delay of 0 still runs later, not in the middle of the script.
Guess the order first. This player replays the order a real run prints, one step at a time.
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");The picture shows the three timers that lines 2 to 4 hand to the browser, and the one that line 5 cancels.
cancel, so its callback never runs.Cancelling matters most when you clean up. Say the user presses Reset, leaves the page, or closes part of the page. Clear every timer that is still waiting. If you don't, an old callback can run later and change a page that has moved on.
Now test the "at least" rule yourself. The lab below sets a 500 ms timer. Then it can run a busy loop: a loop that keeps JavaScript busy for a set time. While the loop runs, nothing else can run, not even a timer that is due.
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 timer. Then pick a busy loop and run it again. The time is measured with performance.now() in your browser when you click.
Repeat with an interval or a nested timeout
INTERACTIVEsetInterval(fn, 500) tries to start fn every 500 ms. That works well for a clock, or for a small check that repeats. But say the work sometimes takes longer than 500 ms. The interval doesn't wait for it. The next callback can start while the last one's work is still going.
A nested timeout is a setTimeout whose callback sets the next timer when its work is done. So each wait starts only after the work ends.
A metronome is a small device that ticks at a steady speed while you practice music. It keeps ticking even if you fall behind. A nested timeout is like a kitchen timer that you set again each time you finish a task. So there is always a full wait after the work.
- In real life: A metronome keeps ticking, even if you miss a beat
- In JavaScript:
setIntervalkeeps starting new callbacks - In real life: Finish chopping, then set the kitchen timer again
- In JavaScript: A nested
setTimeoutwaits until the work is done - In real life: Press Stop on the metronome
- In JavaScript:
clearInterval(id)orclearTimeout(id)
setInterval(async () => {
await saveSlowly();
}, 500);
async function loop() {
await saveSlowly();
timeoutId = setTimeout(loop, 500);
}Lines 1 to 3 use setInterval. Every 500 ms, it starts a new call to saveSlowly(), even if the last save hasn't finished. Lines 5 to 8 use a nested timeout. Line 6 waits for the save with await. Only then does line 7 set the next timer, 500 ms away.
setInterval starts the next save anyway. The nested setTimeout waits for the save to end, then waits 500 ms.Try both below. Each tick does 150 ms of pretend work, and the bars show when each tick really starts.
let intervalId;let intervalTicks = 0;let timeoutId;let timeoutTicks = 0; function doSlowWork() { const end = performance.now() + 150; while (performance.now() < end) {}} intervalId = setInterval(() => { intervalTicks += 1; console.log("interval", intervalTicks); doSlowWork(); if (intervalTicks === 2) clearInterval(intervalId);}, 500); function nestedTick() { timeoutTicks += 1; console.log("nested", timeoutTicks); doSlowWork(); if (timeoutTicks < 2) timeoutId = setTimeout(nestedTick, 500);} timeoutId = setTimeout(nestedTick, 500);No ticks yet.
Press Start. It runs for about five seconds. Every tick does 150 ms of pretend work. Stop, Reset, and leaving the page clear every timer.
So how do you choose?
- Use
setIntervalfor small, steady ticks, like a clock. - Use a nested timeout when each run must finish before the next wait starts.
- Use a nested timeout when the next delay changes each time.
- Hide a small pop-up message 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 you would pick first.
The shortest wait a timer allows
REAL MEASUREMENTSA delay of 0 means "as soon as the browser allows". It doesn't mean "right now". Browsers also have rules about the shortest wait, to protect the page and the computer. When a browser raises a delay that is too small up to a minimum, that is called clamping.
One rule comes from the HTML standard, the rule book that browsers follow. A timer that is set inside another timer's callback is nested. After five levels of nesting, each wait is at least 4 ms. Your measured numbers may be bigger, or they may jump around.
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 timers. You will see how long each one really waits.
Timers keep the delay in a 32-bit number. The largest delay that fits is 2 ** 31 - 1 ms, about 24.8 days. A bigger delay overflows: it doesn't fit, so the timer uses a different delay. In Node.js, setTimeout(fn, 2 ** 31) runs almost at once and shows a TimeoutOverflowWarning. Browsers have the same limit. So don't use one huge timer to wait for weeks.
A negative delay, a missing delay, or NaN ("not a number") doesn't throw an error. The host treats it as a very short delay, like 0. The callback still runs later, and it can still wait behind other code. So use plain, sensible numbers for delays. For a reminder on a calendar date, check the date instead of setting one long timer.
Clearing twice is safe. If you call clearTimeout twice with the same id, nothing breaks and nothing runs again. So a Reset button and a clean-up step can both clear the same timer.
Why the wait is only a minimum
MEASUREHere is the rule you can trust: a timer waits at least the delay you asked for. It can run later when:
- the script that is running now is still busy,
- another callback is running,
- the tab is hidden and the browser slows its timers down,
- or the computer is busy with other work.
Browsers often slow the timers in a hidden tab down to about once per second. Some browsers slow them even more.
That is why every demo in this lesson measures time with performance.now(). It gives the number of milliseconds since the page opened. The results you see are measured on this computer, in this tab, when you click. They are not copied from someone else's computer.
A hidden tab may pause its timers or slow them down to save battery. So don't count ticks to know how much time has passed. For a countdown, save the time when it should end. Each time a callback runs, work out end - performance.now() and show that.
Timer callbacks also have a rule about this. Inside a method, this is the object before the dot, like user in user.start(). But the host calls a timer callback on its own, with no object before the dot. So a normal function callback doesn't get your object as this. An arrow function has no this of its own. It uses the this of the code around it.
const user = {
name: "Asha",
start() {
setTimeout(() => console.log(this.name), 0);
},
};
user.start();Line 8 calls user.start(), so inside start, this is user. The arrow callback on line 4 uses that same this. So when it runs, it prints Asha.
In Node.js, the id has extra methods, such as unref(). They belong to Node.js, not to browsers. To keep your code working everywhere, only pass the id to the matching clear function.
Some tools can be stopped with an abort signal. An abort signal is an object that says "stop now" once it is aborted. fetch, which loads data from a server, is one of these tools.
const controller = new AbortController();
const timeout = AbortSignal.timeout(1000);
console.log(timeout.aborted);
controller.abort();
console.log(controller.signal.aborted);Line 1 makes an AbortController, an object you can abort by hand. Line 2 makes a signal that aborts itself after 1000 ms. Line 3 prints false, because no time has passed yet. Line 4 aborts the controller by hand, so line 5 prints true.
AbortSignal.timeout(1000) makes a signal that is aborted after 1000 ms. You can give it to fetch, or to another tool that listens for an abort signal. It can't cancel a setTimeout timer. For that, use clearTimeout with the id.
The table puts the timer functions side by side. Read each row as: this function runs this, you cancel it like this, and it gives back this.
| Function | What does it run? | How do you cancel it? | What does it give back? | Good to know |
|---|---|---|---|---|
setTimeout(fn, delay, ...args) | It runs fn once, after at least delay ms. | Call clearTimeout(id). | A number in browsers. A Timeout object in Node.js. | Values after the delay are passed to fn when it runs. |
setInterval(fn, delay, ...args) | It runs fn again and again, trying to start every delay ms. | Call clearInterval(id). | A number in browsers. A Timeout object in Node.js. | It never stops by itself. You must clear it. |
Nested setTimeout | It runs once, then sets the next timer when its work is done. | Save the newest id and call clearTimeout(id). | The same as setTimeout. | It is a good fit for asking a server again and again without overlap. |
setImmediate(fn) (Node.js only) | It runs fn soon, after Node.js finishes its current round of input and output work. | Call clearImmediate(id). | An Immediate object. | Browsers don't have it. |
Where you’ll use timers
Timers show up all over web pages. You use them to:
- hide a small pop-up message after a few seconds,
- wait for the user to stop typing before you search,
- ask a server for new data again and again,
- show a countdown,
- or stop a loading spinner if something takes too long.
Two other lessons go deeper. Closures in loops, timers & memory explains a common trap with loop variables. Losing this explains why setTimeout(obj.method) loses its object.
To poll a server means to ask it for new data again and again. The code below polls with a nested timeout, so two requests never overlap.
let timeoutId;
let stopped = false;
async function poll() {
await loadLatestData();
if (!stopped) timeoutId = setTimeout(poll, 1000);
}
poll();
function stopPolling() {
stopped = true;
clearTimeout(timeoutId);
}Line 5 waits for the data to load. Line 6 sets the next timer, but only if polling hasn't stopped. stopPolling does two things: line 12 sets stopped to true, so no new timer is set, and line 13 cancels the timer that is waiting. Real apps call it from a Reset button, or when the user leaves the page.
Debounce means waiting for a quiet moment before you act. A search box can use it, so three quick key presses cause one save, not three. A similar idea is throttle: let an action run at most once in each time window. Throttle suits updates while the user scrolls or resizes the window. The small debounce below keeps only the last value.
let timerId;
function saveSearch(text) {
clearTimeout(timerId);
timerId = setTimeout(() => console.log("save", text), 10);
}
saveSearch("c");
saveSearch("ca");
saveSearch("cat");Each call to saveSearch clears the timer before it on line 3, then sets a new one on line 4. The calls on lines 7 and 8 set timers that the next call clears. Only the timer from line 9 is left, so the code prints save cat once.
save cat prints.For work that changes the page while the user scrolls, requestAnimationFrame is often a better fit. It runs your function just before the browser draws the next picture of the page. Timers don't know when the browser draws.
Common mix-ups
- “0 ms means right now.” It means later: after the script ends, when the browser can run the callback.
- “500 ms means exactly 500 ms.” It means at least 500 ms. Busy code and hidden tabs can make it later.
- “An interval waits for my async work.” It doesn't know when your promise finishes. Use a nested timeout, or a flag that skips a tick while the work is still going.
- “Clearing a timer after it ran undoes it.” Clearing only cancels a timer that is still waiting. Once a callback has run, clearing can't undo it.
- “A huge delay is a safe long reminder.” Delays above
2 ** 31 - 1ms overflow, in Node.js and in browsers. - “Timer ids look the same everywhere.” Browsers give back numbers. Node.js gives back 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, sets two timers, and 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, one number each second. Use a nested timeout, and write a function that cancels it.
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();Each tick sets only one next timer, so only one timer is ever waiting. cancelCountdown clears that id, so a Reset button can stop the countdown.
Rewrite the interval as a nested timeout, so a slow request can't overlap the next one.
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 waits for the data first, and then sets the next timer. The stopped flag and clearTimeout let you stop it.
Here input is a text box and preview is an element on the page. Change the code so the preview 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 key press cancels the waiting update and sets a new one. Only a timer that lasts 300 ms with no key press updates the preview. A later lesson, Debounce & throttle, covers this in full.
Predict what prints. Like a Reset button, the code clears one timer but leaves another one waiting.
const id = setTimeout(() => console.log("save"), 10);
clearTimeout(id);
setTimeout(() => console.log("done"), 0);Only done prints. clearTimeout(id) cancels the save timer before it can run.
Check your understanding
7 QUESTIONSQuestion 1 of 7In
setTimeout(fn, 500), what does the 500 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 pairs of set and cancel functions go together?
Choose an answer to see the explanation.
Question 4 of 7Why can a nested
setTimeoutbe better thansetIntervalfor asking a server again and again?Choose an answer to see the explanation.
Question 5 of 7What do the HTML rules say about many nested
setTimeout(fn, 0)calls?Choose an answer to see the explanation.
Question 6 of 7What usually happens to timers in a browser tab you can't see?
Choose an answer to see the explanation.
Question 7 of 7In a browser, what does
setTimeoutgive back?Choose an answer to see the explanation.
Key takeaways
setTimeoutruns a callback once, later.clearTimeoutcancels it if it hasn't run yet.setIntervalrepeats until you callclearInterval. A nested timeout waits the full delay after each run's work ends.- A delay is the shortest wait, not an exact time. Measure with
performance.now()when the timing matters. - Deeply nested timers wait at least 4 ms, hidden tabs slow timers down, and huge delays overflow.
- Clear your timers on Reset, on Stop, and when the user leaves the page.
Final definition: a JavaScript timer is a reminder kept by the host. It runs a callback later, after at least the delay, and it never pauses the code that is running now.
Up next: The event loop.