Event Loop & Call Stack
How JavaScript handles async operations with a single thread β task queue, microtask queue, and the render cycle.
JavaScript is single-threaded, yet it handles thousands of concurrent operations. The secret is the Event Loop β a continuous cycle that coordinates the Call Stack, Microtask Queue, and Macrotask Queue to execute code without blocking.
JavaScript is single-threaded. The engine executes code top-to-bottom, pushing function calls onto the Call Stack. Each function runs to completion before the next one starts.
When you call setTimeout, fetch, or addEventListener, the browser's Web APIs handle the async work outside the JS thread. When complete, the callback is placed into a queue.
Promises (.then, .catch, .finally), MutationObserver, and queueMicrotask() callbacks go into the Microtask Queue. This queue is drained COMPLETELY after every task β before the browser renders or picks up the next macrotask.
setTimeout, setInterval, I/O, and UI events go into the Macrotask Queue. The Event Loop picks ONE macrotask per cycle, executes it, then drains all microtasks before the next macrotask.
1) Run all synchronous code β 2) Drain the entire Microtask Queue β 3) Browser may render β 4) Pick ONE Macrotask β 5) Repeat. This is why Promise callbacks always run before setTimeout(cb, 0).
Key Concepts
LIFO data structure tracking function execution. JavaScript can only do one thing at a time β the current stack frame.
High-priority queue for Promise callbacks and queueMicrotask. Drained completely between every task.
Lower-priority queue for setTimeout, setInterval, I/O callbacks. One task processed per event loop tick.
The orchestrator that checks: is the stack empty? If yes, drain microtasks, then pick one macrotask. Repeat forever.
Each function on the call stack runs fully before yielding. You can't interrupt synchronous code mid-execution.
1// Classic event loop puzzle:2console.log("1: Script start");34setTimeout(() => console.log("5: setTimeout"), 0);56Promise.resolve()7 .then(() => console.log("3: Promise 1"))8 .then(() => console.log("4: Promise 2"));910console.log("2: Script end");1112// Output order: 1, 2, 3, 4, 513// Why? Microtasks (Promises) run BEFORE macrotasks (setTimeout)14// even though setTimeout(cb, 0) was registered first!
Understanding the Event Loop is foundational to debugging async bugs, preventing UI freezes, and writing performant JavaScript. Every Promise, setTimeout, fetch, and event handler flows through this mechanism.
Common Pitfalls
1UI Freezing from Long Synchronous Operations
Your React app processes a 10,000-row CSV file using a synchronous loop. The UI completely freezes β buttons don't respond, animations stutter, and users think the app crashed.
The synchronous loop occupies the Call Stack for ~3 seconds. Since JavaScript is single-threaded, the Event Loop cannot process any UI events (clicks, scrolls) or render frames until the loop finishes. The browser literally cannot update the screen.
Break the work into chunks using requestIdleCallback or setTimeout(chunk, 0). Each chunk processes ~500 rows, then yields back to the Event Loop, allowing the browser to process pending UI events and render between chunks. Alternatively, move the processing to a Web Worker (separate thread).
Takeaway: Understanding the Event Loop reveals why long sync operations freeze the UI. The loop can't reach the 'render' or 'pick macrotask' phase if the call stack is never empty. Always yield for heavy computation.
2Race Condition Between setTimeout and Promises
You register a setTimeout(cb, 0) to update state, then a Promise.then() to read that state. The Promise callback sees stale data because it runs BEFORE the timeout β even though setTimeout was called first.
Developers assume 'setTimeout(cb, 0)' means 'run immediately after current code'. But 0ms just means 'enqueue in the Macrotask Queue as soon as possible'. Promise callbacks go to the Microtask Queue, which has higher priority.
Never rely on execution order between different queue types. If order matters, chain operations using the same mechanism (all Promises, or all setTimeouts). For reading after a write, use await or .then() chaining to guarantee sequence.
Takeaway: The Microtask Queue is always drained before any Macrotask runs. This queue priority model explains most 'unexpected execution order' bugs in async JavaScript code.