setTimeout(fn, 5000) does not guarantee 5 seconds. It guarantees at least 5 seconds. If the stack is busy, it could be much longer.
You can read Why setTimeout Cannot Be Trusted and try the first 1 exercise on the free plan. Upgrade to unlock the remaining 2 exercises and continue the full lesson flow.
Pro: every unit of every course · unlimited hearts · no weekly limit on assessment attempts
Complete lesson & earn 250 PX
EXERCISE
1setTimeout(fn, 5000) does NOT guarantee that fn runs after exactly 5 seconds. It guarantees that fn will run after AT LEAST 5 seconds. It could be longer.
setTimeout(fn, 5000) does not promise to run fn in five seconds. It promises not to run it sooner than five seconds. The delay is a minimum, and the actual wait can be considerably longer.
The reason follows from the event loop. After the delay elapses, the callback joins the callback queue. It runs when the stack is empty and the microtask queue has been drained. If the thread is busy, it waits.
console.log("start");
setTimeout(function () {
console.log("timer fired at " + (Date.now() - t0) + "ms");
}, 100);
const t0 = Date.now();
const end = t0 + 2000;
while (Date.now() < end) {
// busy for two seconds
}
console.log("blocking finished");
The timer asked for 100 milliseconds and reports roughly 2000. Its delay expired on schedule and the callback was queued on time, then sat there because the thread was occupied by the loop.
Nothing here is broken. The timer did what it said: it waited at least 100 milliseconds.
The same is true of a zero delay:
console.log("first");
setTimeout(function () {
console.log("third");
}, 0);
console.log("second");
Zero means "as soon as possible", and the soonest possible moment is after the current code has finished. There is no way to ask for sooner, because interrupting running code is precisely what a single thread does not do.
Browsers also impose a floor. Nested timers are clamped to about four milliseconds once you go a few levels deep, and background tabs are throttled much harder, often to once per second or less. A tab that is not visible is not worth spending battery on.
This is why timers are the wrong tool for measuring elapsed time:
let ticks = 0;
setInterval(function () {
ticks += 1;
}, 1000);
After an hour, ticks will be less than 3600, and how much less depends on how busy the page was and whether it was in the background. If you need to know how much time has passed, ask the clock:
const started = Date.now();
setInterval(function () {
const seconds = Math.floor((Date.now() - started) / 1000);
console.log(seconds + "s elapsed");
}, 1000);
Counting invocations measures how often the callback ran. Subtracting timestamps measures time.
The delay is a minimum, not a guarantee. setTimeout(fn, 0) still goes through the queue. A busy call stack delays all queued callbacks. The browser is multi-threaded; is not.