Timers & Animation Frames
Some code should not run right now. A message should disappear after three seconds, a countdown should tick every second, a search should wait until the user stops typing, and a ball should glide across the screen. JavaScript has three tools for running code later: setTimeout, setInterval and requestAnimationFrame.
In this lesson you will use all three, learn how to cancel them, and see two patterns that real apps use every day: a countdown and a debounced search box.
setTimeout: run once, later
setTimeout(callback, delay) runs the callback once, after at least delay milliseconds (1,000 ms is one second). Any extra arguments after the delay are passed to the callback. It returns a timer ID, a number you can pass to clearTimeout() to cancel the timer before it fires.
console.log("Order placed");
setTimeout(() => {
console.log("Your chai is ready!");
}, 2000);
setTimeout((name, table) => {
console.log(`${name}, table ${table} is ready`);
}, 1000, "Priya", 7);
const reminder = setTimeout(() => {
console.log("This line never prints");
}, 1500);
clearTimeout(reminder);
console.log("Waiting...");Order placed Waiting... Priya, table 7 is ready Your chai is ready!
Look at the order. "Waiting..." prints second, even though it is the last line. setTimeout does not pause the program: it only schedules the callback and returns immediately. Your code carries on, and the callback runs later. You will see exactly how this works in How Asynchronous Code Works.
The delay is a minimum, not a promise. A timer cannot interrupt code that is already running, so if the page is busy, the callback waits. Browsers also slow timers down in background tabs to save battery. Even setTimeout(fn, 0) waits until the current code has finished.
setInterval: repeat until cancelled
setInterval(callback, delay) runs the callback again and again, every delay milliseconds, until you call clearInterval() with its ID.
let overs = 0;
const intervalId = setInterval(() => {
overs++;
console.log(`Over ${overs} bowled`);
if (overs === 3) {
clearInterval(intervalId);
console.log("Drinks break!");
}
}, 500);Over 1 bowled Over 2 bowled Over 3 bowled Drinks break!
An interval that is never cleared runs until the page closes. Always keep the ID and decide when the interval should stop.
A countdown that stays accurate
Because each tick can arrive a little late, counting ticks slowly drifts away from real time. A better countdown remembers when it should end and calculates the time left on each tick from the clock with Date.now():
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Resend countdown</title>
</head>
<body>
<p>We sent a 6-digit code to your phone.</p>
<button type="button" id="resend" disabled>Resend code</button>
<span id="wait"></span>
<script>
const button = document.querySelector("#resend");
const wait = document.querySelector("#wait");
let timerId = null;
function startCountdown(seconds) {
const endsAt = Date.now() + seconds * 1000;
button.disabled = true;
function tick() {
const left = Math.ceil((endsAt - Date.now()) / 1000);
if (left <= 0) {
clearInterval(timerId);
button.disabled = false;
wait.textContent = "";
return;
}
wait.textContent = `You can resend in ${left}s`;
}
clearInterval(timerId); // never run two countdowns at once
tick(); // show the first number straight away
timerId = setInterval(tick, 250);
}
button.addEventListener("click", () => {
wait.textContent = "Code sent again.";
startCountdown(10);
});
startCountdown(10);
</script>
</body>
</html>Two habits in this code are worth copying. It calls clearInterval before starting a new interval, so clicking quickly can never create two countdowns fighting each other. And it ticks every 250 ms but displays whole seconds, so the number changes close to the real second boundary.
Debouncing: wait until the user pauses
A search box that contacts a server on every key press wastes work: typing "Pune" would send four searches. Debouncing means "wait until things go quiet, then act once". Each new key press cancels the previous timer and starts a new one, so the search runs only after the user pauses.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Debounced search</title>
</head>
<body>
<label>Search stations <input id="search" type="search"></label>
<p id="typed">Key presses: 0</p>
<p id="result">Start typing...</p>
<script>
function debounce(fn, delay) {
let timerId;
return (...args) => {
clearTimeout(timerId); // cancel the previous plan
timerId = setTimeout(() => fn(...args), delay); // and make a new one
};
}
const stations = ["New Delhi", "Rohtak Junction", "Jaipur Junction", "Pune Junction", "Lucknow Charbagh", "Ernakulam Junction"];
let presses = 0;
const search = debounce((text) => {
const matches = stations.filter((s) => s.toLowerCase().includes(text.toLowerCase()));
document.querySelector("#result").textContent =
`Searched for "${text}" at ${new Date().toLocaleTimeString("en-IN")}: ${matches.join(", ") || "no match"}`;
}, 400);
document.querySelector("#search").addEventListener("input", (event) => {
presses++;
document.querySelector("#typed").textContent = `Key presses: ${presses}`;
search(event.target.value);
});
</script>
</body>
</html>Type "pune" quickly: the key-press count goes up to 4, but the search runs only once. The debounce function works because of a closure: the returned function remembers timerId between calls (see Scope, Hoisting & Closures).
requestAnimationFrame for smooth animation
For animation, timers are the wrong tool. Screens redraw at a fixed rate, often 60 times a second and sometimes 120 or more, and a timer does not line up with those redraws, so movement looks jerky.
requestAnimationFrame(callback) asks the browser to call your function just before the next redraw. The browser passes a timestamp in milliseconds. To keep animating, the callback requests another frame. The browser also pauses frames in hidden tabs, which saves battery. cancelAnimationFrame(id) stops a frame that has been requested.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Animation frame</title>
<style>
.pitch { position: relative; block-size: 40px; inline-size: 300px; background: #c8e6c9; border-radius: 20px; }
.ball { position: absolute; inset-block-start: 5px; inline-size: 30px; block-size: 30px; border-radius: 50%; background: crimson; }
</style>
</head>
<body>
<div class="pitch"><div class="ball" id="ball"></div></div>
<p><button type="button" id="bowl">Bowl</button> <span id="info"></span></p>
<script>
const ball = document.querySelector("#ball");
const info = document.querySelector("#info");
const distance = 270; // pixels to travel
const duration = 1500; // milliseconds
let frameId = null;
function bowl() {
cancelAnimationFrame(frameId); // stop any delivery already in progress
let start = null;
let frames = 0;
function step(timestamp) {
start ??= timestamp;
frames++;
const progress = Math.min((timestamp - start) / duration, 1); // 0 to 1
ball.style.translate = `${progress * distance}px`;
if (progress < 1) {
frameId = requestAnimationFrame(step); // ask for the next frame
} else {
info.textContent = `Done in ${frames} frames`;
}
}
frameId = requestAnimationFrame(step);
}
document.querySelector("#bowl").addEventListener("click", bowl);
</script>
</body>
</html>The position is calculated from the elapsed time, not by adding a few pixels per frame. That way the ball takes 1.5 seconds on every screen. A 60 Hz screen shows about 90 frames and a 120 Hz screen about 180, but the ball arrives at the same moment.
Common mistakes
- Calling the function instead of passing it.
setTimeout(showMessage(), 1000)runsshowMessagestraight away. WritesetTimeout(showMessage, 1000). - Passing a string of code.
setTimeout("save()", 1000)works likeeval, which is slow and unsafe. Always pass a function. - Forgetting to clear an interval. It keeps running forever, and every extra click may start another one.
- Expecting exact timing. A delay is a minimum. Use
Date.now()or the frame timestamp for anything that must match real time. - Animating with
setInterval. UserequestAnimationFrameand base the movement on elapsed time.
Key points
setTimeoutruns a function once after at least the given delay;clearTimeoutcancels it.setIntervalrepeats until you callclearIntervalwith its ID.- Timers never pause your code; the callback runs later, after the current code finishes.
- Calculate countdowns from the clock, not by counting ticks.
- Debouncing cancels and restarts a timer so work happens once, after the user pauses.
requestAnimationFramesyncs animation with screen redraws; base movement on elapsed time.
Exercise
Make a "flash sale" banner that says "Sale ends in 15s" and counts down once per second. When the time runs out, change the text to "Sale over" and stop the interval. Add a "Hide" button that hides the banner and also stops the countdown, so no timer keeps running in the background.
Show solution
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Flash sale</title>
</head>
<body>
<p id="banner">Sale ends in 15s</p>
<button type="button" id="hide">Hide</button>
<script>
const banner = document.querySelector("#banner");
const endsAt = Date.now() + 15000;
const timerId = setInterval(() => {
const left = Math.ceil((endsAt - Date.now()) / 1000);
if (left <= 0) {
clearInterval(timerId);
banner.textContent = "Sale over";
} else {
banner.textContent = `Sale ends in ${left}s`;
}
}, 1000);
document.querySelector("#hide").addEventListener("click", () => {
clearInterval(timerId);
banner.hidden = true;
});
</script>
</body>
</html>Both paths call clearInterval, so the interval always stops, whether the time runs out or the user hides the banner first.
Quick Quiz
1. What does this print first: setTimeout(() => console.log("A"), 0); console.log("B");?
A timer callback always waits until the current code has finished, even with a delay of 0.
2. How do you stop a running setInterval?
setInterval returns an ID; clearInterval(id) cancels it.
3. What does debouncing do for a search box?
Each key press cancels the previous timer, so only the last one survives and runs.
4. Why is requestAnimationFrame better than setInterval for animation?
Frames line up with the screen's refresh, giving smoother motion and saving power in background tabs.
Learn JavaScript with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
