Explain the JavaScript event loop: call stack, Web APIs, callback queue, microtask queue
The full model: the call stack runs synchronous code; Web APIs handle async work off-thread; completed callbacks land in either the callback (macrotask) queue or the microtask queue; the event loop, when the stack is empty, drains all microtasks then runs one macrotask, repeating. Each component has a distinct role.
This question wants you to name and connect every component of the JS concurrency model. Here's the whole picture.
The components
1. Call stack — A LIFO stack of execution frames. JS is single-threaded, so only the top frame runs. Synchronous code runs here, start to finish.
2. Web APIs — Browser-provided capabilities (setTimeout, fetch/XHR, DOM events, geolocation, IntersectionObserver...). They run outside the JS engine, often on browser threads. JS calls them and immediately moves on; they don't block the stack.
3. Callback queue (a.k.a. task / macrotask queue) — When a Web API finishes (timer fires, click happens, I/O completes), its callback is placed here. FIFO.
4. Microtask queue — A separate, higher-priority queue for Promise reactions (.then/catch/finally), await continuations, queueMicrotask, and MutationObserver callbacks.
5. Event loop — The coordinator that moves callbacks from the queues onto the call stack — but only when the stack is empty.
How they interact
call a Web API
│
┌──────▼──────┐ finishes ┌───────────────────┐
│ Web APIs │ ─────────────────▶ │ macrotask queue │
└─────────────┘ └───────────────────┘
Promise resolves ┌───────────────────┐
──────────────────────────▶ │ microtask queue │
└───────────────────┘
event loop: if stack empty →
drain ALL microtasks, then run ONE macrotaskThe loop, precisely
- Execute synchronous code on the call stack until empty.
- Drain the microtask queue completely (microtasks spawned mid-drain also run).
- Render, if the browser decides it's time.
- Dequeue one macrotask, push it on the stack, run it.
- Back to step 2.
Example tying it together
console.log("A");
setTimeout(() => console.log("B"), 0); // -> Web API -> macrotask queue
Promise.resolve().then(() => console.log("C")); // -> microtask queue
console.log("D");
// A, D (sync) ... C (microtask) ... B (macrotask)Senior framing
A strong answer doesn't just list the boxes — it explains the contract: the stack must be empty before the loop acts, microtasks always beat macrotasks, and rendering is interleaved between macrotasks (after microtasks drain). That's the model that lets you predict output order and reason about jank.
Follow-up questions
- •Why is there a separate microtask queue instead of one queue?
- •What kinds of work go on Web API threads vs. the JS thread?
- •When exactly can the browser render?
Common mistakes
- •Conflating the callback (macrotask) queue with the microtask queue.
- •Thinking Web APIs run on the JS thread.
- •Forgetting microtasks drain fully before each macrotask.
Edge cases
- •requestAnimationFrame callbacks run in their own phase, just before paint — not in either queue.
- •Microtasks queued during microtask draining still run in the same drain cycle.
Real-world examples
- •Predicting console output order; understanding why Promise chains delay timers.