Skip to content
AtomicReps

Treat timer delays as minimums

A lesson from The Event Loop You Stopped Drawing. Play it above, or read it through below.

  • setTimeout(fn, 0) requests a minimum, not an instant

    The common read: setTimeout(fn, 0) runs the callback at the zero-millisecond mark, essentially "now"

    You already know what a parked task waits for. Name it before reading on: it's the one thing the last lesson built its whole model around.

    setTimeout hands its number to the runtime as a countdown, not as a place in line. That countdown runs off the call stack entirely, which is why it can finish while the stack is still busy running your synchronous code. Finishing the countdown does exactly one thing: it makes the callback eligible to run, and eligible work parks in the task queue, the same resting place any ready work waits in.

    The stack has to run to completion and empty out before the loop even looks at that queue. So a callback can sit there, fully ready, for as long as the running code takes, whether that's a tight loop or a slow synchronous parse. The countdown was never a promise about when the callback runs. It only promises when the countdown ends.

    Read "0" the same way you'd read any other number here: as a floor. setTimeout(fn, 0) means "make this eligible as soon as possible", and "as soon as possible" is still bounded by whatever's already running.

  • Equal-delay timers don't race. They queue in order.

    The common read: two timers set for the same delay race each other, so their order is unspecified or up to the runtime

    The task queue is a plain line, first in, first out. A timer's delay decides the moment it's allowed to join that line; it says nothing about where it lands once it's there. Two timers set for the same delay become eligible at the same moment, and a FIFO line breaks that tie the only way it can: whoever joined first comes out first. a was called before b, so a goes first, every time.

    The same rule explains setTimeout(fn, 0). It isn't a fast lane and it doesn't jump the line; it means "make this eligible after the current run finishes, and after everything already ahead of it in the queue." That's the entire trick behind using a zero-delay timer to split one long job into pieces: each piece gets an honest turn in line rather than a shortcut. A later lesson turns that into a real technique for keeping a page responsive.

    One caveat, worth a single line because it changes nothing above: browsers can raise the floor further than you asked. Per the HTML spec's timer steps, a timer nested five levels deep is held to at least 4ms no matter what delay you passed, and a background tab can be throttled harder still. The number you hand setTimeout is a floor everywhere it's honored. It is never a guarantee.

  • setTimeout(0) is an ordering tool, not a speed setting

    The common read: setTimeout(0) schedules the callback "as soon as possible", ahead of anything else already waiting.

    You schedule setTimeout(f, 0) while three tasks already sit in the task queue. When does f run? You can answer this from the first lesson's law before reading on.

    It runs fifth: after the function currently on the stack, and after all three tasks that were queued before it. A delay of 0 buys no priority. The task queue is FIFO, and 0 only means "park me now"; parking now still means parking at the back.

    Read that way, setTimeout(0) stops looking like a speed setting and becomes an ordering tool: it moves work after the current run and after everything already queued, on purpose. This is the honest version of the folk fix "wrap it in a setTimeout and it works": the wrap didn't make the code faster or luckier, it moved the code behind whatever the current turn still had pending, which was the actual problem all along.

  • setTimeout is not a frame clock

    A number that's only a floor is the wrong tool for anything that needs to land ON time rather than merely not-before it: an animation tick, a fixed heartbeat, a countdown a user is staring at. setTimeout(fn, 16) doesn't mean "every 16ms". It means "no sooner than 16ms after this call", and each call's own overhead, plus whatever else is running first, plus whatever floor the browser adds, compounds across a chain of them. Round numbers make dependable-looking promises they never actually made.

    The tool built for landing exactly on time hooks the browser's own paint cycle instead of guessing at a delay. A later lesson names it and shows what it buys you that a timer can't.