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.
console.log("before");
const id = setTimeout(() => console.log("after 500 ms"), 500);
console.log("after, right away");
// before
// after, right away
// after 500 msThe 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.
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 msA 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.
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
}
}
}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
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.
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.
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.