Event Loop: Micro vs Macrotasks
JavaScript can only do one thing at a time, yet it handles timers, network calls and clicks without freezing. The event loop is how. It also explains the output order of those "predict the output" questions.
Definition
JavaScript runs on a single thread. It has one call stack where code executes. Slow work, like timers and network requests, is handed to the browser. When that work is done, a callback is placed in a queue. The event loop is the loop that moves callbacks from the queues onto the call stack, but only when the stack is empty.
Analogy: a restaurant with one chef. Orders (callbacks) wait in a line. The chef finishes the current dish, then takes the next order. VIP orders (microtasks) always go first.
The Parts
1Call Stack → what is running right now2Web APIs → browser features: timers, fetch, DOM events (run outside the stack)3Macrotask queue → callbacks from setTimeout, setInterval, events, I/O4Microtask queue → callbacks from Promises, queueMicrotask, MutationObserver5Event loop → moves the next task onto the stack when it is emptyThe Order of Work
After each piece of code finishes, the event loop does this:
11. Run all synchronous code until the stack is empty22. Run ALL microtasks (and any new ones they add) until the microtask queue is empty33. Maybe render the page44. Take ONE macrotask, run it55. Go back to step 2The key rule: the microtask queue is emptied completely before the next macrotask.
| Microtasks | Macrotasks | |
|---|---|---|
| Sources | .then, catch, await, queueMicrotask | setTimeout, setInterval, click events, I/O |
| Priority | Higher | Lower |
| Between tasks | Run all | Run one |
Predict the Output
1console.log("1");2 3setTimeout(() => console.log("2"), 0);4 5Promise.resolve().then(() => console.log("3"));6 7console.log("4");Output: 1 4 3 2.
11. "1" and "4" are synchronous → they print first22. The timer callback goes to the macrotask queue33. The promise callback goes to the microtask queue44. Stack is empty → microtasks run first → "3"55. Then the macrotask → "2"A harder one with await:
1async function run() {2 console.log("a");3 await null;4 console.log("b"); // runs as a microtask after the current code5}6run();7console.log("c");8// a c bEverything after an await is like a .then, so it runs as a microtask.
setTimeout(fn, 0) Is Not Instant
0 means "as soon as possible after the stack and microtasks are clear". Browsers also enforce a small minimum delay for nested timers. A timer is a minimum wait, not a guarantee.
Blocking the Loop
Here is what breaks. A long synchronous task blocks everything, because nothing can move from the queue until the stack is empty.
1button.onclick = () => {2 for (let i = 0; i < 1e9; i++) {} // page freezes, no clicks, no rendering3};Fixes: split work into chunks, move it to a Web Worker, or do it on the server.
A microtask that keeps adding microtasks can also starve the page, because rendering waits until the microtask queue is empty.
In React
setStateupdates are batched and applied after your handler finishes. Your handler is a macrotask, so React renders after it.useEffectruns after the browser paints, so it never blocks the first paint.useLayoutEffectruns before paint.- A heavy calculation during render blocks the whole UI. That is why
useTransitionanduseDeferredValueexist: they let React split urgent and non-urgent work. awaitinside an effect or handler resumes as a microtask, after the current code finishes.
Summary
1JS is single threaded → one call stack2Slow work → Web APIs → callback waits in a queue3Event loop → moves queued callbacks to the stack when it is empty4Microtasks (promises, await) → run ALL before the next macrotask5Macrotasks (timers, events) → run ONE at a time6Order → sync code, then microtasks, then one macrotask, repeat7Long sync code → blocks clicks and renderingPromises sit at the heart of the microtask queue, so they are the next topic.