JavaScript setTimeout and setInterval
Learn how to schedule code in JavaScript with setTimeout and setInterval: syntax, arguments, canceling timers, recursive patterns, and debouncing/throttling.
Sometimes you don't want to run code right now — you want to run it later, or run it repeatedly. JavaScript's two scheduling functions, setTimeout() and setInterval(), let you do exactly that. This guide covers their syntax, how to pass arguments, how to cancel a scheduled timer, the important "recursive setTimeout" pattern, the surprising behavior of a zero delay, and two real-world uses: debouncing and throttling.
Neither function is part of the core JavaScript language — they are provided by the host environment (browsers and Node.js). The behavior described here is the same in both, with a few differences noted along the way.
Introduction to JavaScript Timing Functions
A timer schedules a callback to run after the current code finishes and a given amount of time has passed. Because JavaScript is single-threaded, the callback never interrupts running code; it waits in a queue and runs only when the call stack is empty. (For the full mechanism, see The Event Loop.)
The setTimeout() Function
setTimeout() runs a function once after a specified delay. It accepts a function to execute and a delay in milliseconds before that execution.
Syntax
let timerId = setTimeout(func, delay, arg1, arg2, ...);func— the function (or, less commonly, a string of code) to run.delay— the wait in milliseconds before running. Defaults to0.arg1, arg2, ...— optional arguments passed straight tofunc.
The return value is a numeric timer id you can later pass to clearTimeout().
Example
Passing arguments to the callback
Anything you put after the delay is forwarded to the callback. This is cleaner than wrapping the call in another arrow function:
setTimeout(greet(), 1000) runs greet() immediately and schedules its return value (likely undefined). Write setTimeout(greet, 1000) — no parentheses.The setInterval() Function
setInterval() has the same signature as setTimeout(), but instead of running the callback once, it runs it repeatedly every delay milliseconds until you stop it.
Syntax
let timerId = setInterval(func, delay, arg1, arg2, ...);Example
setTimeout pattern below.Recursive setTimeout vs. setInterval
You can reproduce setInterval() by having a setTimeout() callback re-schedule itself. The key difference: setInterval() measures the delay between starts, while recursive setTimeout() measures it between the end of one run and the start of the next — guaranteeing a fixed pause even when the callback is slow.
Canceling Scheduled Execution
Both functions return a timer id. Hold on to that id and you can cancel the scheduled work with clearTimeout() or clearInterval(). (The two clear functions are actually interchangeable in most engines, but matching them to the scheduler that created the id keeps the code readable.)
Stopping setTimeout()
To cancel a timeout, store the id returned by setTimeout() and pass it to clearTimeout() before the delay elapses.
Example
Stopping setInterval()
Similarly, save the id from setInterval() and pass it to clearInterval(). Without this, the interval runs forever (or until the page closes), which is a common source of memory leaks and runaway timers.
Example
The zero-delay setTimeout
setTimeout(func, 0) does not run func immediately. It schedules func to run as soon as the current synchronous code has finished. This is a handy way to "yield" — to let the browser repaint or to break a long task into pieces — and it explains output ordering that often surprises beginners:
Note that timers are macrotasks: they run after all queued microtasks (such as resolved promise callbacks). See Event loop: microtasks and macrotasks for the precise ordering rules.
Practical Applications and Tips
Two of the most common real-world uses of setTimeout() are debouncing and throttling — both are ways to limit how often a function runs in response to rapid-fire events.
Debouncing with setTimeout()
Debouncing waits until a burst of events has stopped before running the function. Each new event resets the timer, so the callback fires only after things go quiet for wait milliseconds. This is ideal for a search box: you want to send one request after the user stops typing, not one per keystroke.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Debounced Input Example</title>
<script>
// Debounce function to limit the rate at which a function is executed
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// Function to be debounced
function fetchData(input) {
alert(`API call with input: ${input}`); // Placeholder for an API call
}
// Create a debounced version of fetchData
const debouncedFetchData = debounce(fetchData, 300);
// Add the debounced function to an event listener
function setup() {
document.getElementById('searchInput').addEventListener('input', (event) => {
debouncedFetchData(event.target.value);
});
}
// Ensure setup is called once the document is fully loaded
document.addEventListener('DOMContentLoaded', setup);
</script>
</head>
<body>
<h3>Type in the input field:</h3>
<input type="text" id="searchInput" placeholder="Start typing..." />
</body>
</html>Throttling with setTimeout()
Throttling runs the function at most once per limit milliseconds, no matter how many events arrive in between. Where debouncing waits for silence, throttling guarantees a steady cadence — perfect for scroll, resize, or mousemove handlers that would otherwise fire dozens of times a second. The example below uses a leading-edge approach (it runs immediately on the first event, then enforces the gap):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Throttled Scroll Event</title>
<style>
/* Simple styling for demonstration */
body, html {
height: 200%; /* Make the page scrollable */
margin: 0;
padding: 0;
font-family: Arial, sans-serif;
}
#log {
position: fixed;
top: 0;
left: 0;
background: white;
border: 1px solid #ccc;
padding: 10px;
width: 300px;
}
</style>
</head>
<body>
<div id="log">Scroll to see the effect...</div>
<script>
// Throttle function using setTimeout
function throttle(func, limit) {
let lastFunc;
let lastRan;
return function() {
const context = this;
const args = arguments;
if (!lastRan) {
func.apply(context, args);
lastRan = Date.now();
} else {
clearTimeout(lastFunc);
lastFunc = setTimeout(function() {
if ((Date.now() - lastRan) >= limit) {
func.apply(context, args);
lastRan = Date.now();
}
}, Math.max(0, limit - (Date.now() - lastRan)));
}
}
}
// Function to be throttled
function handleScroll() {
const log = document.getElementById('log');
log.textContent = `Scroll event triggered at: ${new Date().toLocaleTimeString()}`;
}
// Add event listener for scroll
window.addEventListener('scroll', throttle(handleScroll, 1000));
</script>
</body>
</html>Gotchas to remember
- Delays are a minimum, not a guarantee. If the call stack is busy or the event loop is congested, the callback waits. The number you pass is the earliest it can run, not a promise.
- Background tabs are throttled. Most browsers clamp timers in inactive tabs to roughly once per second to save power, so animations and polling slow down when the tab is hidden.
- Nested timeouts are clamped to ~4ms. After five nested
setTimeout()calls, browsers enforce a minimum delay of about 4 milliseconds, so a0delay is never truly zero in deep chains. - Maximum delay. A delay larger than
2147483647(2^31 − 1) overflows the 32-bit field and is treated as0, firing almost immediately instead of far in the future. thisbinding. When you pass a method likesetTimeout(obj.method, 1000), it loses itsthis. Use an arrow function —setTimeout(() => obj.method(), 1000)— orobj.method.bind(obj).- Always clean up. Clear intervals (and pending timeouts) when a component unmounts or the work is no longer needed, or you'll leak timers and may operate on stale state.
Related topics
- Introduction: callbacks — the foundation timers are built on.
- The Event Loop: microtasks and macrotasks — why a
0ms timeout still runs last. - Promise and Async/await — modern alternatives for sequencing asynchronous work.
- Recursion and stack — background for the recursive
setTimeoutpattern.
Conclusion
setTimeout() runs code once after a delay; setInterval() runs it on a repeating schedule; clearTimeout() and clearInterval() cancel them. Remember that delays are minimums, pass arguments after the delay rather than inside a wrapper, reach for the recursive setTimeout pattern when you need steady spacing, and always clean up timers you no longer need. With debouncing and throttling on top, these two small functions cover most time-based work you'll do in the browser.