Inside one task you call setTimeout(a, 100), then setTimeout(b, 100), then setTimeout(c, 50). In what order do the three callbacks run, and what rule decides it?
answer
- due time first, then registration
- the tie-break is FIFO
- each timer clock starts at its call
- one callback per task
- blocking delays but never reorders
basics
~20 sOrder is decided by expiry time, with ties broken by registration order: c runs first because it expires 50 ms earlier, then a, then b. Registration order only matters between timers that come due at the same moment.
solid answer
~50 sThey run c, then a, then b. Each timer's clock starts when `setTimeout` is called, so all three were scheduled at essentially the same instant and their due times are +50, +100 and +100. The earliest due time wins, which puts c first. Between a and b the due times are equal, and the host queues equal-expiry timers in the order they were registered — so a precedes b. That FIFO tie-break is dependable, and it is what makes it safe to schedule two zero-delay timers and rely on the first one running first. Each callback is its own task, so a microtask checkpoint runs between them; if a queues a promise callback, that promise callback runs before b. And if the thread were blocked past all three deadlines, they would still come out in the same c, a, b order — everything just runs late together.
go deeper
Be able to predict the output of a few setTimeout calls with different delays, and say that the shortest delay wins because every countdown starts at the moment of its own call.
Explain the two-part rule — due time first, registration order as the tie-break — and that each callback occupies its own task with a microtask checkpoint after it. Know that a blocked thread delays timers without reordering them.
Demonstrate that you never encode sequencing in delay numbers. Talk about expressing required ordering structurally, and about reading correlated lateness across timers as evidence that something is holding the thread.
Own the guidance: delays chosen to "land after" some other work are latent race conditions that surface under load or on slow devices. Push for explicit sequencing primitives and for tests that run with an artificially blocked loop.
## The rule in one line Timers are ordered by **when they come due**, and among timers that come due at the same moment, by **when they were registered**. ## Working the example ```js setTimeout(() => console.log('a'), 100); setTimeout(() => console.log('b'), 100); setTimeout(() => console.log('c'), 50); // c // a // b ``` All three registrations happen inside one synchronous task, microseconds apart, so treat the scheduling instant as identical. Due times are therefore t+50, t+100, t+100. Sorting by due time puts c first. a and b tie, and the tie is broken by insertion order, so a comes out ahead of b. The part people get wrong is assuming the *call* order decides everything — that a, registered first, must run first. It does not. What is registered first only wins when nothing else is due earlier. The converse mistake is assuming ties are arbitrary. They are not: equal-expiry timers preserve insertion order. That is why `setTimeout(f, 0); setTimeout(g, 0);` reliably runs f before g, and why splitting work across several zero-delay timers keeps a predictable sequence. ## Each callback is its own task When the loop is free at t+100 and both a and b are due, it does **not** run them together. It takes one task, runs a, and then performs the microtask checkpoint before picking up the next task. Consequences: ```js setTimeout(() => { console.log('a'); Promise.resolve().then(() => console.log('a-micro')); }, 0); setTimeout(() => console.log('b'), 0); // a // a-micro // b ``` The promise callback queued by a runs before b's timer callback, because microtasks are drained between tasks. And an exception thrown inside a does not prevent b from running — it becomes an uncaught error for that task alone, and the loop carries on. ## When the loop is blocked Suppose the thread is busy until t+1000. At that point all three timers are long overdue. They do not merge and they do not get dropped: the host runs them one task at a time, still ordered c, a, b. So a blocked loop delays timers, but does not reorder them relative to one another. The important corollary is that lateness is shared. If you see one timer fire hundreds of milliseconds late, every other timer due in that window is late by a similar amount — which is a useful diagnostic when a page suddenly feels unresponsive. ## The clock starts at the call, not at the previous timer A related confusion: people read three `setTimeout` calls as a sequence, as if the second delay began when the first callback ran. It does not. Every call starts its own countdown from the moment of the call. To run things in sequence with gaps between them you have to schedule each from inside the previous callback, or await something. ```js // NOT a 50/100/150 staircase — all three start now setTimeout(one, 50); setTimeout(two, 100); setTimeout(three, 150); // This one is a staircase, because each is scheduled when the previous ran setTimeout(() => { one(); setTimeout(() => { two(); }, 50); }, 50); ``` The first form is fine and common, but only because the absolute due times happen to be staggered. ## What you can and cannot rely on **Can rely on:** relative order of timers registered in the same task with the same delay; a shorter delay coming first when both are registered together; each callback getting its own task with a microtask checkpoint after it. **Cannot rely on:** the absolute time at which any of them runs; a small difference in requested delay actually mapping to a difference in firing time once host floors and clamps are applied; ordering against timers registered in some *other* task whose scheduling instant you do not control. That last point matters in real code: two modules that each schedule "in 100 ms" from different callbacks have no guaranteed relationship. If sequencing between two pieces of work is a requirement, express it with a promise chain or by scheduling the second from inside the first — not by picking delay numbers and hoping.
- If two timers with the same delay are both overdue because the thread was blocked, do they run in one task or two?Two. The loop takes one timer task, runs its callback, performs the microtask checkpoint, and only then picks up the next. That is why a promise callback queued inside the first timer runs before the second timer's callback, and why an exception in the first does not stop the second from running.
- Can you rely on the ordering between a timer scheduled in one task and a timer scheduled in a different task?Only through their absolute due times, which you rarely control precisely. If the second task ran an unknown amount of time later, its timer's countdown started later too, so the relative order depends on scheduling jitter. When order actually matters, chain the work — schedule the second from inside the first, or sequence it with promises.
- Does setTimeout(f, 100) followed by setTimeout(g, 101) reliably run f before g?On due time, yes — 100 comes before 101, and both were scheduled together. But a 1 ms difference is far below the noise floor of real scheduling, and host clamps can flatten small delays. Treat it as an accident of arithmetic, not a design tool; if the order is load-bearing, express it structurally instead.
saying these in an interview costs you the question
- Says callbacks always run in the order setTimeout was called
- Claims order among equal delays is unspecified or random
- Thinks each timer's countdown starts when the previous one fires
- Assumes overdue timers are coalesced into a single task
- Believes an exception in one timer callback cancels the others