setTimeout and setInterval in JavaScript
setTimeout schedules a callback once after a minimum delay; setInterval calls it repeatedly on every interval tick until you stop it.
Theory
TL;DR
setTimeoutfires once, like a kitchen timer for one pizza.setIntervalfires forever, like an oven alarm that beeps every 5 minutes until you turn it off.- Main difference: one-shot vs. repeating.
- Need to run something once? Use
setTimeout. Need a loop? UsesetInterval, but chainsetTimeoutfor better control with async work. - Both return an ID you can pass to
clearTimeoutorclearIntervalto cancel. - The delay is a minimum, not a guarantee. The event loop may add more time.
Quick example
// setTimeout: fires ONCE after delay
const timeoutId = setTimeout(() => console.log("Fired once"), 100);
// clearTimeout(timeoutId); // cancel before it fires
// setInterval: fires REPEATEDLY every interval
const intervalId = setInterval(() => console.log("Tick"), 1000);
setTimeout(() => clearInterval(intervalId), 5000); // stop after ~5 tickssetTimeout adds one task to the queue. setInterval keeps adding a new task every interval, whether the previous one finished or not.
Key difference
setInterval schedules the next call the moment each interval elapses. It does not wait for the callback to finish. If your callback takes 1.5 seconds and the interval is 1 second, calls start queuing up behind each other. setTimeout fires once and stops. To get a repeating timer that actually waits for the callback to complete, chain setTimeout inside itself.
When to use
- Hide a loading spinner after a delay:
setTimeout. - Poll an API every 5 seconds:
setIntervalfor simple cases, or chainedsetTimeoutwhen the callback is async. - Debounce a search input: chain
setTimeoutwithclearTimeouton each keystroke. - Smooth 60fps animation:
requestAnimationFrame, not timers. - Avoid blocking the UI: any timer beats a
whileloop.
Comparison table
| Feature | setTimeout | setInterval |
|---|---|---|
| Execution | Once after delay | Repeatedly every interval |
| Minimum delay | ~4ms (browsers clamp nested calls) | ~4ms, but calls can overlap |
| Cancellation | clearTimeout(id) | clearInterval(id) |
| Async safety | Predictable | Can pile up if callback is slow |
| When to use | Debounce, one-off delays, cleanup | Clocks, simple polling |
How the browser handles this
setTimeout and setInterval are not part of the JavaScript engine. They live in the browser's Web API layer (or libuv in Node.js). When the delay elapses, the browser places the callback on the macrotask queue. The event loop picks it up only after the current call stack and all microtasks (Promises) are clear. That is why setTimeout(fn, 0) does not run immediately. Zero milliseconds is not zero time.
Browsers also enforce a minimum ~4ms delay for nested setTimeout calls beyond 5 levels deep (HTML5 spec). In background tabs, browsers throttle timers even more, sometimes up to 1000ms.
Common mistakes
Passing a function call instead of a reference
function greet(name) { console.log(`Hi ${name}`); }
setTimeout(greet("Alice"), 1000); // runs immediately, passes undefined to setTimeout
setTimeout(() => greet("Alice"), 1000); // correctgreet("Alice") executes right away and returns undefined. You are passing that result to setTimeout, not the function itself.
Forgetting cleanup in React components
// Memory leak: interval survives unmount
useEffect(() => {
setInterval(() => fetchData(), 5000);
}, []);
// Correct
useEffect(() => {
const id = setInterval(fetchData, 5000);
return () => clearInterval(id);
}, []);The interval keeps running after the component unmounts. The fix is one line: return a cleanup function.
Using setInterval with slow async callbacks
// Calls can overlap if fetch takes longer than 5s
setInterval(async () => {
const data = await fetch("/api/status");
updateUI(data);
}, 5000);
// Better: wait for each call to finish
function poll() {
fetch("/api/status")
.then(res => res.json())
.then(data => {
updateUI(data);
setTimeout(poll, 5000);
});
}
poll();Trying to change the interval from inside the callback
let delay = 1000;
setInterval(() => {
delay *= 2; // does nothing - interval was fixed at scheduling
}, delay);
// If you need dynamic delays, chain setTimeout:
let delay = 1000;
function tick() {
console.log("Tick");
delay *= 2;
setTimeout(tick, delay);
}
setTimeout(tick, delay);Real-world usage
- React search input:
useEffect+setTimeoutfor 300ms debounce,clearTimeoutin cleanup. - Redux DevTools:
setIntervalfor periodic state snapshots. - VS Code: chained
setTimeoutfor typing debounce in autocomplete. - Express/Node.js: graceful shutdown timers,
setTimeoutto force-close after a cleanup window. - For animations use
requestAnimationFrame; for real-time data use WebSockets instead of polling.
Follow-up questions
Q: Why does setTimeout(fn, 0) not run immediately?
A: It pushes the callback to the macrotask queue. The event loop runs all sync code and microtasks first, then picks it up. Zero delay means "as soon as possible after current work", not "right now".
Q: What is the ~4ms clamp?
A: The HTML5 spec throttles nested setTimeout calls (after 5 levels) to a minimum of 4ms. Background tabs can be throttled to 1000ms or more to save CPU.
Q: When should you prefer chained setTimeout over setInterval?
A: Any time the callback is async or takes variable time. Chaining guarantees the next call starts only after the previous one finishes, so calls never overlap.
Q: How do you cancel setInterval safely when the ID is captured in a closure?
A: Store the ID in a variable outside the callback: const id = setInterval(...); setTimeout(() => clearInterval(id), 5000). Never rely on the callback to store its own ID.
Q: In a tab where tasks take 100ms each, what happens to setInterval(..., 10)?
A: Callbacks queue up but do not run in parallel since JS is single-threaded. The event loop processes them one by one, so actual intervals end up around 100ms, not 10ms. You get a backlog, not concurrency.
Examples
Basic: deferred single action
console.log("Start");
const id = setTimeout(() => console.log("Fired"), 100);
console.log("End");
// Output: Start -> End -> Fired
// "End" prints before "Fired" because setTimeout is asyncsetTimeout does not block. The engine moves on to console.log("End") immediately, and fires the callback later from the macrotask queue.
Intermediate: debounced search input in React
function SearchInput() {
const [query, setQuery] = useState("");
useEffect(() => {
const id = setTimeout(() => {
if (query) fetchResults(query);
}, 300);
return () => clearTimeout(id); // cancel if user keeps typing
}, [query]);
return <input onChange={e => setQuery(e.target.value)} />;
}
// Each keystroke resets the timer.
// The API call fires only after 300ms of silence.Every time query changes, the old timer is cancelled and a new one starts. The request goes out only when the user pauses.
Advanced: chained setTimeout vs setInterval for API polling
// setInterval version - can overlap if fetch is slow
const id = setInterval(async () => {
const res = await fetch("/api/health");
const data = await res.json();
console.log(data.status);
}, 2000);
// Chained setTimeout - waits for each response
function pollHealth() {
fetch("/api/health")
.then(r => r.json())
.then(data => {
console.log(data.status);
setTimeout(pollHealth, 2000); // next poll after this one completes
})
.catch(() => setTimeout(pollHealth, 5000)); // back off on error
}
pollHealth();The chained version lets you adjust the delay based on the result and handle errors cleanly. I have seen the setInterval version cause double-requests in production when servers get slow under load. One call was still in flight when the next one fired.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.