A countdown, a clock, a data refresh every thirty seconds: these needs look identical, and yet one of them copes badly with the obvious solution.
setInterval is easy to write and easy to misuse. Understanding what it guarantees, and above all what it does not, avoids most of the trouble.
Definition
setInterval(callback, delay) asks the environment to reschedule the same function every delay milliseconds, indefinitely, until clearInterval is called with the id it returned.
let left = 3;
const id = setInterval(() => {
console.log(left);
left--;
if (left === 0) {
clearInterval(id);
console.log("done");
}
}, 1000);
// 3, 2, 1, doneThe stopping condition lives inside the callback: without it, the interval runs for as long as the page is open, or for as long as the process lives under Node.js.
The drift that sets in
The interval counts down from the scheduling, not from the end of the callback. If the work takes longer than the delay, the requested rhythm becomes impossible to hold and the gap widens with every pass.
const t0 = Date.now();
let pass = 0;
const id = setInterval(() => {
pass++;
const start = Date.now();
while (Date.now() - start < 30) {} // 30 ms of work in a 10 ms interval
console.log("pass", pass, "at", Date.now() - t0, "ms");
if (pass === 3) clearInterval(id);
}, 10);The passes never land at 10, 20 and 30 milliseconds: each one starts after the previous has finished. A clock built on a count of passes therefore drifts, where a clock that rereads Date.now stays accurate.
The version that never overlaps
For work of variable duration, a network call for instance, you reschedule yourself with setTimeout() after each run. The delay becomes a pause between two passes, which makes any pile-up impossible.
async function refreshInLoop(step) {
while (true) {
await readData(); // unknown duration
await new Promise((r) => setTimeout(r, step)); // pause afterwards
}
}
// or, without async:
function loop(step) {
setTimeout(() => { doTheWork(); loop(step); }, step);
}A forgotten interval is a classic leak. In a component-based interface, every setInterval created on display has to be canceled on removal, otherwise it keeps running against a screen that no longer exists and fires updates into the void.
Frequently asked questions
Can two runs overlap?
Not on the thread: the event loop runs one thing at a time, so two passes never execute together. The Promise a pass starts, a request for example, can perfectly well still be in flight when the next pass begins. That is where the data gets mixed up.
What happens in a background tab?
Browsers space out the intervals of a hidden tab considerably, often to one run per second at best. A countdown built on a count of passes will therefore show a wrong value on return: always recompute from a starting timestamp.
Should a recursive setTimeout be preferred?
As soon as the duration of the work is uncertain, yes. setInterval remains perfectly suited to short, predictable work such as refreshing a display. For anything touching the network, the recursive version avoids passes treading on each other.