The Event Loop
Understand how JavaScript schedules asynchronous work through the call stack, task queues, and microtasks.
Call Stack and Single Threading
JavaScript runs user code on a single call stack per thread. Functions push frames when called and pop when they return. Long-running synchronous code blocks the stack and freezes the UI in browsers or delays I/O in Node.js.
Offload heavy computation to Web Workers in browsers or worker threads in Node.js. Never perform expensive synchronous loops on the main thread in interactive applications.
- One call stack per thread
- Sync code blocks everything on that thread
- Use workers for CPU-heavy work
function a() { b(); }
function b() { console.log('done'); }
a(); // a → b → log → pop → popMacrotasks and the Callback Queue
When async APIs like `setTimeout`, `setInterval`, I/O callbacks, and UI events complete, their callbacks enter macrotask queues. The event loop picks the next macrotask only when the call stack is empty and microtasks are drained.
This is why `setTimeout(fn, 0)` does not run immediately — it waits until current synchronous code and all pending microtasks finish. Understanding this ordering prevents race conditions in initialization code.
- Macrotasks: timers, I/O, DOM events
- Processed after stack is empty and microtasks done
- `setTimeout(0)` is not instant
console.log('1');
setTimeout(() => console.log('2'), 0);
console.log('3'); // 1, 3, 2Microtasks and Priority
Microtasks include Promise callbacks (`.then`, `.catch`, `.finally`), `queueMicrotask`, and `MutationObserver` handlers. After each macrotask, the engine drains the entire microtask queue before taking the next macrotask.
This priority explains why `Promise.resolve().then(...)` runs before `setTimeout` even when both are scheduled during the same synchronous block. Microtask starvation can occur if microtasks recursively schedule more microtasks without yielding.
- Microtasks run before next macrotask
- Promises schedule microtasks
- Avoid infinite microtask loops
Promise.resolve().then(() => console.log('micro'));
setTimeout(() => console.log('macro'), 0);Browser vs Node.js Event Loops
Browsers integrate the event loop with rendering pipelines — `requestAnimationFrame` callbacks run before paint. Node.js has multiple phases in its libuv loop: timers, pending callbacks, poll, check, and close callbacks, plus separate microtask processing between phases.
Node 11+ processes microtasks between macrotasks similarly to browsers, but phase ordering still matters for `setImmediate` vs `setTimeout` nuances. Read the Node.js docs for your target version when debugging server-side timing issues.
- Browsers tie loop to rendering
- Node has libuv phases
- Microtask timing differs slightly by runtime
Debugging Async Ordering
When logs appear out of expected order, map each async operation to macrotask vs microtask. Use `performance.mark` and async stack traces in DevTools to follow scheduling.
Design APIs that do not depend on subtle ordering between Promises and timers unless documented. Prefer explicit `await` chains in application logic over relying on queue priority for correctness.
- Classify operations as micro vs macro task
- Use DevTools async stacks
- Prefer explicit await over timing hacks