Skip to content
AtomicReps

Treat timer delays as minimums

A lesson from The JavaScript Event Loop: Predicting Execution Order. Play it above, or read it through below.

Level I · How the Event Loop Runs
  • 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: the last lesson's model depends on it.

    setTimeout passes its number to the runtime as a countdown, not as a position in the queue. That countdown runs outside the call stack, which is why it can finish while the stack is still running your synchronous code. Finishing the countdown does one thing: it makes the callback eligible to run, and eligible work parks in the task queue, where any other ready work waits.

    The stack has to run to completion and become empty before the loop checks that queue. So a callback can wait there, ready to run, for as long as the running code takes, whether that code is a tight loop or a slow synchronous parse. The delay you pass states when the countdown ends, not when the callback runs.

    Read "0" the same way as any other delay: as a minimum. setTimeout(fn, 0) means "make this eligible as soon as possible", and the callback still cannot run until the code already running has finished.

  • 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

    Two timers with the same delay do not race. b was set after a, so its countdown cannot finish before a's. The HTML spec also states the rule directly: a timer waits for every earlier timer on the same page whose delay is equal or shorter. Node keeps the same order. a was set before b, so a runs first, every time.

    The same rule explains setTimeout(fn, 0). It does not give the callback priority or move it ahead of other tasks; it means "run this after the current code finishes, and after every earlier timer that is already waiting." That is how a zero-delay timer splits one long job into pieces: each piece waits its turn in the queue like any other task. A later lesson uses this as a technique for keeping a page responsive.

    One caveat, which changes nothing above: browsers can raise the minimum higher than you asked. Per the HTML spec's timer steps, a timer set from inside a chain of more than five nested timer callbacks is held to at least 4ms, whatever delay you passed, and a background tab can be throttled harder still. The number you pass to setTimeout is a minimum in every runtime, never an exact time.

  • 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 call setTimeout(f, 0) while three earlier zero-delay timers are still waiting to run. When does f run? Use this lesson's same-delay rule to answer before reading on.

    It runs fifth: after the code currently on the stack, and after the three timers that were set before it. A delay of 0 gives no priority. Timers with equal delays run in the order they were set, so a new zero-delay timer goes behind every earlier one.

    So setTimeout(0) is an ordering tool, not a speed setting: it moves work after the current run and after every earlier timer that is already waiting, on purpose. That ordering explains the common fix "wrap it in a setTimeout and it works": the wrapper did not make the code faster; it moved the code after the work the current turn still had pending, and that pending work was the cause of the bug.

  • setTimeout is not a frame clock

    A delay that is only a minimum is the wrong tool for anything that needs to run ON time rather than merely no earlier than a set time: an animation frame, a fixed-interval heartbeat, a countdown a user is watching. setTimeout(fn, 16) doesn't mean "every 16ms". It means "no sooner than 16ms after this call", and each call's own overhead, plus any code running first, plus any minimum the browser adds, accumulates across a chain of calls. A round number like 16 looks like a precise interval, but setTimeout never promised one.

    For an animation, the browser has a separate API that runs your callback just before the next repaint, instead of after a delay; a later lesson covers it. For a heartbeat or a countdown a user is watching, keep the timer, but on each run read the current time from a clock and compute the elapsed time from it, instead of adding up the delays you asked for.