Timers let you run code later: hide a message after a few seconds, poll for updates every minute, or move something on screen frame by frame. JavaScript gives you three tools for this. setTimeout runs code once after a delay, setInterval runs it repeatedly, and requestAnimationFrame runs it just before the browser paints the next frame.
setTimeout
setTimeout(callback, delay) runs the callback once, after at least delay milliseconds:
setTimeout(() => {
console.log("Two seconds later");
}, 2000);
Extra arguments after the delay are passed to the callback:
function greet(name) {
console.log(`Hello, ${name}`);
}
setTimeout(greet, 1000, "Ada"); // → Hello, Ada (after one second)
Pass a function, not a call. setTimeout(greet("Ada"), 1000) calls greet immediately and passes its return value, undefined, as the callback.
Cancelling with clearTimeout
setTimeout returns an ID. Pass it to clearTimeout to cancel the timer before it fires:
const id = setTimeout(() => console.log("Never logged"), 1000);
clearTimeout(id);
In browsers the ID is a number. In Node.js it is a Timeout object. Either way, treat it as an opaque value and only pass it back to clearTimeout. Calling clearTimeout with an ID that has already fired, or with undefined, does nothing, so it is always safe.
A status message that clears itself
The course dashboard shows a "Saved" message when someone saves a course. It should disappear after a few seconds, and if they save another course quickly, the new message should get its own full display time:
const status = document.querySelector("#status");
let clearStatusTimer;
function showStatus(message, ms = 5000) {
clearTimeout(clearStatusTimer);
status.textContent = message;
clearStatusTimer = setTimeout(() => {
status.textContent = "";
}, ms);
}
showStatus("Saved JavaScript Basics");
Without the clearTimeout, the first message's timer would wipe the second message early.
Give people time to read. Messages that vanish after a second or two are missed by slow readers and by anyone who looked away. For anything important, such as an error, leave it visible until the user acts.
The delay is a minimum
The delay is the earliest time the callback can run, not an exact time. A timer callback only runs when the main thread is free. If other code is busy for 500ms, a 100ms timer waits until it finishes:
const start = Date.now();
setTimeout(() => {
console.log(`Ran after ${Date.now() - start}ms`);
}, 100);
while (Date.now() - start < 500) {
// busy for half a second
}
// → Ran after 500ms (or slightly more)
Browsers also add limits of their own:
- Deeply nested timers (a timeout that schedules a timeout, many levels deep) are clamped to a minimum of about 4ms.
- Timers in background tabs are throttled, often to once per second or less, to save battery.
So never use timers to measure time. If you need to know how much time has passed, compare timestamps from Date.now() or performance.now().
setTimeout(callback, 0) does not run immediately either. It runs after the current code and after any pending promise callbacks, as lessons 31 and 42 explain.
setInterval
setInterval(callback, delay) runs the callback repeatedly, every delay milliseconds, until you stop it with clearInterval:
let count = 0;
const id = setInterval(() => {
count += 1;
console.log(`Tick ${count}`);
if (count === 3) clearInterval(id);
}, 1000);
// → Tick 1
// → Tick 2
// → Tick 3
Do not count ticks to keep time
Because each tick can be late, adding one second per tick drifts away from real time, especially in a background tab. Calculate from a fixed end time instead:
function startCountdown(seconds, onTick, onDone) {
const endTime = Date.now() + seconds * 1000;
const id = setInterval(() => {
const remaining = Math.max(0, Math.ceil((endTime - Date.now()) / 1000));
onTick(remaining);
if (remaining === 0) {
clearInterval(id);
onDone();
}
}, 250);
return () => clearInterval(id); // a function the caller can use to stop it
}
const stop = startCountdown(
10,
(s) => console.log(`Seconds left: ${s}`),
() => console.log("Time is up")
);
Checking four times a second and calculating from endTime keeps the display accurate even if some ticks are delayed. Returning a stop function is a tidy way to let the caller cancel the timer without knowing its ID.
If you show a countdown on the page, do not put it in a live region that announces every second. Screen reader users would hear a constant stream of numbers. Announce only meaningful moments, such as "One minute left" and "Time is up".
Repeating with setTimeout
setInterval fires on schedule even if the previous run has not finished its asynchronous work. When each run does something slow, such as a network request, chain timeouts instead, so the next run is only scheduled after the current one completes:
async function poll() {
try {
const response = await fetch("data/courses.json");
if (response.ok) console.log("Checked for updates");
} catch (error) {
console.error(error);
} finally {
setTimeout(poll, 60_000); // schedule the next check only when this one is done
}
}
poll();
60_000 is 60000 written with a numeric separator for readability.
Timers as promises
setTimeout uses a callback, but it is easy to wrap in a promise, as lesson 31 did:
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
await delay(1000);
console.log("One second later");
Node.js ships this ready-made in node:timers/promises:
import { setTimeout as sleep } from "node:timers/promises";
await sleep(1000);
console.log("One second later");
requestAnimationFrame
For anything visual that changes every frame, timers are the wrong tool. A screen typically refreshes 60 times a second or more, and a setTimeout(callback, 16) drifts in and out of step with it, producing uneven, juddering movement.
requestAnimationFrame(callback) asks the browser to call your function once, just before it paints the next frame. To animate, the callback requests the next frame itself:
function frame(timestamp) {
// update something on the page
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
The callback receives a high-resolution timestamp in milliseconds, comparable with performance.now(). Browsers pause requestAnimationFrame in background tabs, so an animation costs nothing while nobody can see it.
Like the other timers, it returns an ID, and cancelAnimationFrame(id) cancels the pending frame.
Base animation on time, not frames
Screens refresh at different rates: 60, 90, 120 times a second or more. If you move something 2 pixels per frame, it moves twice as fast on a 120Hz screen. Instead, work out how far through the animation you are from the elapsed time.
This example counts the dashboard's total lessons up from 0 over one second:
const output = document.querySelector("#total-lessons");
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
function countUp(element, target, duration = 1000) {
if (reduceMotion.matches) {
element.textContent = String(target);
return;
}
let start;
function frame(timestamp) {
start ??= timestamp;
const progress = Math.min((timestamp - start) / duration, 1);
element.textContent = String(Math.round(target * progress));
if (progress < 1) {
requestAnimationFrame(frame);
}
}
requestAnimationFrame(frame);
}
countUp(output, 52); // the six courses in data/courses.json have 52 lessons between them
start ??= timestamprecords the time of the first frame.progressgoes from 0 to 1 overduration, whatever the screen's refresh rate.- The animation stops itself by not requesting another frame once
progressreaches 1. - When the user has asked their system to reduce motion, the final value appears straight away.
Always check prefers-reduced-motion before script-driven animation. CSS media queries cannot switch off motion that JavaScript creates frame by frame. Movement can cause dizziness and nausea for people with vestibular disorders, and for anything that moves or updates automatically for more than five seconds, people also need a way to pause or stop it.
For most interface movement, such as fading, sliding and expanding, CSS transitions and animations are simpler and run more efficiently than JavaScript. Use requestAnimationFrame when the values depend on script logic, such as numbers, canvas drawing or positions calculated from data.
Cleaning up
Timers that are never cleared keep their callbacks, and everything those callbacks refer to, alive. When a part of the page is removed, clear its intervals and cancel its animation frames. Lesson 47 shows how forgotten timers turn into memory leaks, and lesson 48 builds debounce and throttle helpers from setTimeout.
Summary
setTimeout(fn, ms)runs once;setInterval(fn, ms)repeats. Both return an ID forclearTimeoutandclearInterval.- Pass a function, not the result of calling one.
- The delay is a minimum. Busy code, nested timers and background tabs all make timers late.
- Measure elapsed time with timestamps, not by counting ticks.
- Chain
setTimeoutinstead ofsetIntervalwhen each run does slow asynchronous work. requestAnimationFrameruns just before each repaint; base movement on elapsed time and cancel withcancelAnimationFrame.- Check
prefers-reduced-motionbefore animating with JavaScript, and clean up timers when their part of the page goes away.