Scheduling: setTimeout and setInterval
Sometimes you don’t want a function to run at this exact moment. You want it to fire a second from now, or every few seconds, or the instant the current work finishes. That is what scheduling gives you: you hand a function to the environment and say “call this later.”
Two methods cover almost every case:
setTimeoutruns a function once, after a delay you specify.setIntervalruns a function repeatedly, waiting the delay before the first call and then re-firing at that same interval.
Neither method lives in the JavaScript language spec itself. They come from the host environment. The browser provides them, Node.js provides them, and the behavior is close enough that you rarely notice the difference in everyday code.
setTimeout
Here is the full shape of the call:
let timerId = setTimeout(func|code, [delay], [arg1], [arg2], ...)
Each parameter:
func|codeThe function to run later. You can also pass a string of code, which stays supported for historical reasons, but you should not use it. Pass a function.
delayHow long to wait before running, measured in milliseconds. 1000 ms is one second. If you leave it out, the default is
0.
arg1,arg2…Any extra values you list here get passed straight into the function as its arguments when it runs.
This schedules ring() to run one second from now:
function ring() {
alert('Timer done');
}
setTimeout(ring, 1000);
Notice you pass ring, not ring(). You are handing over the function itself so the scheduler can call it later.
Extra arguments after the delay flow into the function:
function greet(phrase, who) {
alert( phrase + ', ' + who );
}
setTimeout(greet, 1000, "Welcome", "Maya"); // Welcome, Maya
If the first argument is a string, the environment builds a function out of it. So this technically runs:
setTimeout("alert('Hello')", 1000);
Don’t do that. A code string is opaque to your tools, skips syntax checking until it runs, and behaves like a mini eval. An arrow function reads better and keeps everything in scope:
setTimeout(() => alert('Hello'), 1000);
Canceling with clearTimeout
Every setTimeout hands back a timer identifier. Keep it, and you can call the schedule off before it fires:
let timerId = setTimeout(...);
clearTimeout(timerId);
Here we schedule something, change our mind, and cancel. The result is that nothing happens at all:
let timerId = setTimeout(() => alert("never happens"), 1000);
alert(timerId); // timer identifier
clearTimeout(timerId);
alert(timerId); // same identifier (it doesn't become null after canceling)
That last line is worth noting: canceling does not wipe the variable. timerId still holds the same value; it just no longer points at a live timer.
What that value is depends on the environment. In a browser it is a plain number. Node.js returns a timer object with its own extra methods. Since there is no shared spec forcing one shape, treat the identifier as an opaque token: store it, pass it to clearTimeout, and don’t rely on what type it is.
Browsers document their timer behavior in the timers section of the HTML Living Standard.
setInterval
setInterval reads exactly like setTimeout:
let timerId = setInterval(func|code, [delay], [arg1], [arg2], ...)
Same arguments, same meanings. The difference is repetition: instead of firing once, it keeps calling the function every delay milliseconds until you stop it with clearInterval(timerId).
This shows a message every two seconds, then shuts itself off after five:
// repeat with the interval of 2 seconds
let timerId = setInterval(() => alert('tick'), 2000);
// after 5 seconds stop
setTimeout(() => { clearInterval(timerId); alert('stop'); }, 5000);
The two calls work together: the interval keeps ticking, and a one-shot timeout cancels it later. Because the interval fires at 2s and 4s, you see two “tick” alerts before “stop” arrives at 5s.
Nested setTimeout
There are two ways to run something on a repeating schedule. One is setInterval. The other is a setTimeout that re-schedules itself:
/** instead of:
let timerId = setInterval(() => alert('tick'), 2000);
*/
let timerId = setTimeout(function tick() {
alert('tick');
timerId = setTimeout(tick, 2000); // (*)
}, 2000);
Line (*) is the trick: at the end of each run, the function schedules its own next run. It never uses setInterval, yet it repeats forever.
This pattern is more flexible than setInterval, because you decide the next delay only once you know how the current call went. Nothing is locked in ahead of time.
Say you poll a server every five seconds, but if the server reports overload you want to back off — 10 seconds, then 20, then 40 — instead of hammering it. Pseudocode:
let delay = 5000;
let timerId = setTimeout(function request() {
...send request...
if (request failed due to server overload) {
// increase the interval to the next run
delay *= 2;
}
timerId = setTimeout(request, delay);
}, delay);
This “back off when things go wrong” idea is common enough to have a name: exponential backoff. setInterval can’t do it, because its delay is fixed the moment you call it. And if the scheduled work is CPU-heavy, you can measure how long it took and plan the next call sooner or later to smooth things out.
Nested setTimeout lets you control the gap between runs more precisely than setInterval. To see why, compare two versions of the same repeating call.
With setInterval:
let i = 1;
setInterval(function() {
func(i++);
}, 100);
With nested setTimeout:
let i = 1;
setTimeout(function run() {
func(i++);
setTimeout(run, 100);
}, 100);
They look equivalent, but the timing differs. setInterval counts its 100 ms from the start of one call to the start of the next. The time func spends running is inside that window:
Did you catch it? The real pause between the end of one func and the start of the next is less than 100 ms — because part of each 100 ms interval is consumed by func running. That is expected behavior, not a bug.
It gets more dramatic if func sometimes takes longer than 100 ms. The engine can’t start the next call while it is still busy with the current one. So it waits for func to finish, then checks the schedule, sees the time is already up, and runs it again immediately. In the extreme case where func always outruns the delay, the calls happen back to back with no pause between them at all.
Nested setTimeout avoids that, because it starts counting only when the current call ends:
Nested setTimeout guarantees a fixed minimum delay (100 ms here) between the end of one call and the start of the next, because the next call is planned only once the previous one is done.
Zero delay setTimeout
There is a special case: setTimeout(func, 0), or the shorthand setTimeout(func). This says “run func as soon as possible.” But “as soon as possible” does not mean “right now.” The scheduler only runs it after the current script finishes.
So the function is queued to fire the moment the running code hands control back. This prints “Hello” first, then “World”:
setTimeout(() => alert("World"));
alert("Hello");
Line one drops the call onto the schedule with a 0 ms delay. But the engine won’t look at the schedule until the current script — including that alert("Hello") on the last line — has completely finished. So the direct alert wins, and the scheduled one runs right after.
There are deeper browser use cases for zero-delay timeouts, covered in Event loop: microtasks and macrotasks.
Summary
setTimeout(func, delay, ...args)runsfunconce afterdelaymilliseconds;setInterval(func, delay, ...args)runs it over and over on that interval. Extraargspass straight intofunc.- Cancel a pending call with
clearTimeout/clearInterval, using the identifier thatsetTimeout/setIntervalreturned. - Nested
setTimeoutis the flexible alternative tosetInterval: you set the next delay at the end of each run, so you can control the gap between executions precisely and adapt it on the fly. setTimeout(func, 0)(same assetTimeout(func)) schedules a call for “as soon as possible, but only after the current script finishes.”- Browsers clamp the minimum delay to 4 ms once you have five or more nested
setTimeoutcalls, or forsetIntervalafter its fifth call. It is a historical quirk.
None of these methods guarantee the exact delay you asked for. A browser timer can slow down for several reasons:
- The CPU is under heavy load.
- The tab is in the background.
- The laptop is running on battery-saver mode.
Any of these can push the effective minimum timer resolution up to 300 ms or even 1000 ms, depending on the browser and the OS-level power settings.