JavaScript runs on a single thread, but the real production pitfall is queue ordering: synchronous work runs first, then microtasks drain before the next macrotask, which is why long Promise chains can starve rendering and confuse debugging.
Explain the JavaScript Event Loop
The Big Picture
The event loop matters most when you have to debug async UI behavior that still feels blocked. JavaScript runs on a single thread, so the runtime decides whether to keep draining microtasks, take the next macrotask, or finally let the browser paint.
That under-the-hood order is the real production pitfall: many developers assume any async boundary yields rendering time, but a long microtask chain can still freeze the screen.
The Three Main Parts
The JavaScript runtime manages execution using three core parts: the Call Stack, Task Queues, and the Event Loop.
Component | Purpose | Examples / Details |
|---|---|---|
Call Stack | Where synchronous code runs, line by line. If a function takes too long here, everything else waits (blocking). |
|
Task Queues (two types) | Where async tasks wait until the stack is clear — split into Microtask and Macrotask queues. | Async callbacks, Promises, timers, I/O events |
Microtask Queue | Smaller, high-priority tasks that run immediately after the stack is clear. |
|
Macrotask Queue | Larger, lower-priority tasks that can yield to the browser for rendering between runs. |
|
The Event Loop
The event loop constantly checks:
- Is the call stack empty?
- Any microtasks waiting? → Run all of them.
- Take one macrotask and run it.
console.log('A');
setTimeout(() => console.log('B (macrotask)'));
Promise.resolve()
.then(() => console.log('C (microtask)'))
.then(() => console.log('D (microtask)'));
console.log('E');
// Output:
// A
// E
// C
// D
// B
Why this order?
AandE→ run first (synchronous stack).
- Then the event loop runs all microtasks →
C,D.
- Finally, the next macrotask runs →
B.
That’s why Promises always run before timers, even when scheduled “at the same time.”
Microtasks vs. Macrotasks (Summary)
Type | Examples | Runs When | Priority |
|---|---|---|---|
Microtask |
| After the current call stack, before rendering | High |
Macrotask |
| After microtasks, allows rendering | Normal |
After each macrotask, all microtasks are drained before the next one starts. That’s why too many microtasks (like recursive Promises) can block rendering.
function loop() {
Promise.resolve().then(loop); // microtask recursion
}
loop(); // browser freezes — it never yields control back
In Node.js
The event loop runs through several phases:
timers → pending callbacks → poll → check → close callbacks
Microtasks (
Promise.then, process.nextTick) run after each phase, unlike browsers where they run once per loop.setTimeout(() => console.log('timer'));
setImmediate(() => console.log('immediate'));
Promise.resolve().then(() => console.log('microtask'));
// Common order:
// microtask → timer → immediate
Imagine you’re the only cashier at a store. You serve one customer at a time (the call stack). When a customer needs to grab something, you tell them to step aside (macrotask). But before you call the next one, you handle quick questions like ‘Can I get a receipt?’ (microtasks). You repeat this all day — that’s the event loop!
- JavaScript executes synchronously on a single thread.
- The event loop coordinates between the stack and queues.
- Microtasks (Promises) always run before macrotasks (timers).
- Too many microtasks can freeze rendering.
- Use macrotasks for deferred work and microtasks for quick follow-ups.
Use the relevant interview-question hub first, then move into a concrete study plan before targeted company sets.