Two queues
The event loop has a task queue for things like timers and events, and a microtask queue for promise reactions and queueMicrotask callbacks.
The key rule
After every task, and after each callback that empties the JavaScript stack, the browser drains all microtasks before doing anything else. Microtasks therefore run before the next task and before the next render.
Ordering example
- A resolved promise then handler is a microtask and runs before a setTimeout of zero, which is a task.
- New microtasks queued while draining are also run in the same drain, before yielding.
The starvation risk
Because the drain continues until the queue is empty, a microtask that keeps scheduling more microtasks can loop forever and never let the browser render. The page locks up even though no single callback is long.
Practical guidance
- Use microtasks for small follow up work that should happen before paint.
- Use a task, such as setTimeout or a message channel, when you deliberately want to yield to rendering.
Key idea
Microtasks drain fully after each task and before rendering, so they run before timers but can starve paint if they keep enqueuing themselves.