One thread, many queues
JavaScript runs your code on a single call stack. When the stack empties, the event loop decides what runs next by pulling from queues. Two kinds of work wait there, and they are not treated equally.
- Macrotasks: timers, message events, and IO callbacks like setTimeout.
- Microtasks: promise reactions and queueMicrotask callbacks.
The draining rule
After each macrotask, the loop drains the entire microtask queue before rendering or taking the next macrotask. New microtasks scheduled during draining run in the same pass, so a microtask flood can starve the next timer.
- Synchronous code runs first to completion.
- Then all pending microtasks run.
- Then one macrotask, and the cycle repeats.
This is why a promise resolution always fires before a setTimeout of zero, even though both look immediate. Understanding the order removes most surprises about when a callback actually runs.
Key idea
After every macrotask the loop empties all microtasks before doing anything else, so promises always beat timers.