setInterval() in JavaScript: repeating work at a regular interval

setInterval() calls a function back every X milliseconds: the drift that sets in, the risk of overlap, and the recursive setTimeout alternative.
3 min read
Believemy logo

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.

JAVASCRIPT
let left = 3;

const id = setInterval(() => {
  console.log(left);
  left--;
  if (left === 0) {
    clearInterval(id);
    console.log("done");
  }
}, 1000);

// 3, 2, 1, done

The 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.

JAVASCRIPT
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.

JAVASCRIPT
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);
}
Good to know

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

Question

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.


Question

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.


Question

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.

Related terms

Discover our javaScript glossary

Every word of JavaScript explained simply: keywords, built-in objects, methods, errors and concepts. Clear definitions and examples that actually run, to learn and to troubleshoot.

Share this article

Want to help us? Share this article on your networks or even better: on your site, in an article or in your newsletter.