setTimeout() in JavaScript: running code after a delay

setTimeout() schedules a callback after a minimum delay: the id and clearTimeout, extra arguments, and the version that hands back a promise.
3 min read
Believemy logo

Dismissing a notification after three seconds, retrying a request after a failure, letting an animation finish before switching screens: plenty of moments call for simply waiting.

The language has no instruction for that, and just as well: freezing the single thread would freeze the whole page. The environment supplies a timer instead.


Definition

setTimeout(callback, delay) hands a function to the environment, which drops it into the task queue once the delay has elapsed, expressed in milliseconds. The next line runs immediately, without waiting.

JAVASCRIPT
console.log("before");

const id = setTimeout(() => console.log("after 500 ms"), 500);

console.log("after, right away");

// before
// after, right away
// after 500 ms

The call returns an id, worth keeping so the timer can be canceled: clearTimeout(id) pulls the callback out of the queue if it has not left yet. Canceling a timer that has already fired raises nothing.


The delay is a floor, not a guarantee

The number you pass says when the callback becomes eligible. It runs at that moment only if the call stack is free, which accounts for every delay you observe.

  • Work in progress delays everything. A long function holds the thread, and the callback waits its turn behind it.
  • Nested delays behave poorly. In a browser, from five nested timers onward, the specification clamps the minimum delay to four milliseconds.
  • A background tab is slowed down. Browsers space out the timers of a hidden tab considerably to save battery.
JAVASCRIPT
const t0 = Date.now();
setTimeout(() => console.log("planned at 50 ms, actual:", Date.now() - t0), 50);

const end = Date.now() + 200;
while (Date.now() < end) {}   // the thread is busy for 200 ms


A version that hands back a promise

A setTimeout wrapped in a Promise becomes a pause usable with await, and the code recovers its vertical reading.

JAVASCRIPT
const pause = (ms) => new Promise((r) => setTimeout(r, ms));

async function retry(work, attempts = 3) {
  for (let i = 0; i < attempts; i++) {
    try {
      return await work();
    } catch (err) {
      if (i === attempts - 1) throw err;
      await pause(200 * (i + 1));   // growing wait between two attempts
    }
  }
}
Good to know

Arguments placed after the delay are passed on to the callback: setTimeout(show, 100, "Chloe", 3) will call show("Chloe", 3). That saves an intermediate arrow function whenever the values are already known.


Frequently asked questions

Question

What is setTimeout(fn, 0) actually worth?

It does not mean right now, it means as soon as the stack is free. It is the usual way to push work past the end of the running code, for instance to let the browser paint an update before a computation starts.


Question

What does the returned id hold?

That depends on the environment: a plain number in a browser, a Timeout object under Node.js. Treat it as an opaque value, to be passed along to clearTimeout unchanged, and never rely on its type in code shared between the two.


Question

Why does my timer use an old value?

Because the callback reads the variable when it runs, not when it was scheduled. Inside a loop, a shared var gives every timer the final value, whereas a let creates one binding per pass and gives the expected result.

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.