One thread, many phases
An event loop runs callbacks on a single thread by pulling work from queues in a strict order. Understanding that order explains surprising scheduling.
Two kinds of queues
- The macrotask queue holds timers, input, and most callbacks.
- The microtask queue holds settled promise reactions and similar urgent work.
After each macrotask the loop drains the entire microtask queue before picking the next macrotask. So a chain of resolved promises all runs before the next timer fires.
Starvation risk
Because microtasks run to exhaustion, a microtask that keeps scheduling more microtasks can starve macrotasks forever. The page or server appears frozen even though the thread is busy.
Practical ordering
Code after an await runs as a microtask, so it executes before a setTimeout of zero scheduled earlier in the same tick. Knowing this lets you reason about race free ordering on a single thread.
Key idea
The event loop runs one macrotask, then fully drains microtasks, then repeats, and microtasks always jump ahead of the next macrotask.