Not all callbacks are equal
The event loop does not use one queue but two priority levels. The task queue holds macro tasks such as timers and io callbacks. The microtask queue holds promise reactions and similar high priority work. The order they drain in shapes observable behavior.
The draining rule
After each task runs, the loop drains the entire microtask queue before picking the next task.
- Run one task from the task queue.
- Then run all queued microtasks until that queue is empty.
- Only then take the next task.
This means a resolved promise handler runs before the next timer, even a zero delay timer scheduled earlier.
Why it matters
Microtasks let promise chains complete promptly between tasks, keeping async logic snappy. But there is a hazard. If a microtask keeps queuing more microtasks, the loop can never reach the next task, a condition called microtask starvation. Tasks, by contrast, always yield back to the loop, allowing rendering and io.
Key idea
The loop runs one task then fully drains the microtask queue before the next task, giving promise reactions priority over timers while risking starvation if microtasks endlessly enqueue more.