JavaScript Event Loop Table

StageWhat runs thereField note
Call stackThe one running functionSingle-threaded means the stack, not a thread pool - blocking code is a busy stack
Microtask queuePromise .then/.catch/.finally, queueMicrotask, await resumesDrains COMPLETELY after each task - before rendering, before the next timer
Macrotask queuesetTimeout, setInterval, events, I/O callbacksONE task per loop turn - timers are a minimum wait, never exact
requestAnimationFrameVisual callbacks, before the next paintSynced to the frame, not to the timer queue - cancelled when the tab hides
RenderingStyle, layout, paintHappens after microtasks drain and rAF runs - skipped entirely for hidden tabs
StarvationMicrotasks scheduling microtasksqueueMicrotask(self) never yields - tasks AND rendering starve; timers re-queue, so they yield
Reference: the MDN event loop reference. The model decodes every interview question at once: one loop picks ONE task, runs it to completion, drains the ENTIRE microtask queue, then maybe renders. setTimeout(fn, 0) fires after promise callbacks because timers are tasks while .then is a microtask - microtasks always go first, regardless of who was scheduled first. Bottom line: every await is a microtask boundary (the rest of the function resumes as a .then), a long task blocks everything because the stack must empty before any other work, and rAF exists so visual changes sync to the frame instead of racing the timer queue. Related tools: JS timers table (the macrotask side in detail), fetch table (promise results arriving through microtasks), and iterator table (pull-based flow vs the loop's push-based tasks).

JavaScript runs one loop, not threads: it picks ONE task, runs it to completion on the call stack, drains the entire microtask queue, then - only if something changed and the tab is visible - renders. Every ordering surprise in asynchronous JavaScript decodes from that sequence.

Bottom line: the famous setTimeout(fn, 0) vs promise ordering question is just queue priority - timers are macrotasks, .then callbacks are microtasks, and the microtask queue always drains completely between tasks, so promises cut line no matter who registered first. Every await is the same mechanism: the rest of the function resumes as a microtask.

The honest part: single-threaded does not mean slow - it means a long task blocks EVERYTHING. A 200-millisecond loop on the stack delays timers, click handlers and rendering alike, which is why chunking work across tasks (or moving it to a worker) is the real fix, not a faster machine.

How to use

  1. Order code deliberately: if a callback must run after the current task but before any rendering, put it in queueMicrotask or a promise .then.
  2. Chunk long work: move batches into setTimeout(fn, 0) steps so each task stays short - the loop gets breathing room for events and paint.
  3. Use requestAnimationFrame for visual changes - it runs once per frame before paint, instead of racing timers that may fire twice or not at all.

Frequently asked questions

Why does a promise .then fire before setTimeout(fn, 0)?

Queue priority. setTimeout registers a MACROtask - the loop picks up one macrotask per turn. Promise reactions are MICROtasks, and after the current task finishes, the loop drains the entire microtask queue before touching the next macrotask. So even a timer registered first waits behind every microtask that exists at drain time. The classic interview snippet (setTimeout log, promise log, sync log) resolves as: sync first (the stack), then the microtask, then the timer - exactly one rule, zero memorization of examples.

What counts as a microtask and what counts as a task?

Microtasks: promise reactions (.then/.catch/.finally), await resumptions, queueMicrotask callbacks and MutationObserver. Tasks: setTimeout and setInterval callbacks, event handlers (click, input, message), I/O completions and most everything the platform schedules. The design intent: microtasks are 'finish the current logical operation' steps - a promise chain should settle completely before the browser moves on - while tasks are independent units of work the scheduler can interleave with rendering and input.

Can the event loop starve, and how?

Yes - schedule a microtask that schedules itself (queueMicrotask(step) at the end of step) and the loop drains that queue forever: no timers, no events, no rendering. Microtasks must terminate; that is the contract. Timers cannot starve the loop this way because each setInterval/setTimeout callback is a separate TASK, and between tasks the loop serves everything else. Infinite while loops starve it harder still - the stack never empties - which is the same failure at a coarser grain.

Where does requestAnimationFrame fit relative to timers?

rAF callbacks run in the RENDERING step, once per frame, after microtasks have drained - not in the timer queue at all. That placement guarantees your visual change is applied exactly once per painted frame: a resize handler fired five times in one frame plus a rAF callback runs the callback once. Timers cannot promise that (a 16ms interval can fire 0 or 2 times per frame when the frame rate drifts). This is why animation code moved from setInterval to rAF decades ago - it syncs to when pixels actually update.

Related tools