Imagine a tiny store with one cashier. Customers line up in different ways: some are standing in the main line, some leave sticky notes that must be handled right after the current customer, and the cashier also has to occasionally tidy the store window so it looks updated.
The browser works like that too. It handles JavaScript one thing at a time, then checks for urgent follow-up work, and only then gets a chance to render what changed on the page. That’s why a small piece of code can still affect when a page updates, and why some callbacks seem to run "before" others even if they were scheduled earlier.
What the event loop actually coordinates
The event loop is the browser’s scheduling system for JavaScript work. It coordinates three big pieces: the call stack where current JS runs, the task (macrotask) queue for things like timers and user events, and the microtask queue for promise reactions and queueMicrotask.
This matters because JavaScript on the main thread is single-file: if one task runs too long, it blocks timers, input handling, and painting. Interviewers ask this to see whether you know that "async" does not mean "runs immediately"—it means "runs later, in a specific queue, with specific priority."
1) The call stack runs first
When JS starts executing, it goes on the call stack. As long as the stack is busy, nothing else can interrupt it.
That means scheduled work does not preempt running code. If you write a long loop, the browser cannot process timers, microtasks, or paint updates until that loop finishes. In interviews, this is the key reason why "the event loop" is not magic parallelism: it is coordination around a single active JS thread.
2) Tasks and microtasks are not equal
When the stack becomes empty, the browser looks for pending work. A task (also called a macrotask) is a chunk of work from sources like setTimeout, DOM events, or message events. A microtask is higher-priority follow-up work, especially Promise callbacks and queueMicrotask.
The rule to remember is: after one task finishes, the browser drains all microtasks before starting the next task. That is why Promise.then(...) usually runs before setTimeout(..., 0), even if both were queued during the same turn.
3) `setTimeout(0)` is not immediate
setTimeout(fn, 0) means "schedule fn in a future task, after at least the timer delay and after the current work finishes." The 0 does not mean run now.
In practice, timer callbacks are often delayed by other work, clamping rules, or the fact that the browser must first finish the current task and any queued microtasks. So setTimeout(0) is a way to yield to the event loop, not a way to jump ahead of everything else.
4) `queueMicrotask` runs before the next task
queueMicrotask puts work into the microtask queue, alongside Promise reaction callbacks. This makes it useful when you want to run follow-up logic as soon as the current JS finishes, but still before timers and rendering-related task boundaries.
Because microtasks drain completely, you can accidentally starve the browser if you keep adding more microtasks from inside microtasks. Interviewers sometimes use this to test whether you know that microtasks are "higher priority" but not free—they can delay paint and input just like long synchronous code can.
5) Rendering happens between turns
The browser can only paint when it gets a chance between pieces of JS work. After a task finishes and microtasks are drained, the browser may update layout and render the screen before moving to the next task.
This is why blocking the main thread hurts responsiveness: even if data is "ready," the user may not see it until JS yields. A long-running task can delay both input handling and visual updates, which is why performance-minded code tries to keep tasks short.
Predict the output
Consider this ordering puzzle:
A synchronous log runs first.
queueMicrotask(...) and Promise.resolve().then(...) schedule microtasks.
setTimeout(..., 0) schedules a task.
The expected output is:
synchronous log
microtask from queueMicrotask
microtask from Promise.then
timer callback
If you add nested microtasks inside a microtask, those nested microtasks still run before the timer. That is the classic interview trap: people expect setTimeout(0) to "come next," but microtasks always win after the current task ends.
Suppose a click handler does this: updates state, then runs a 200 ms busy loop, then schedules a timer. Even if the DOM state changed at the start, the browser cannot paint the updated UI until the handler returns and the microtasks are drained.
So the user sees the old screen longer than expected. This is the practical interview answer to "why is blocking the main thread bad?": it delays timers, input, microtasks completion, and rendering all at once.