Event Loop Deep Dive
The event loop is JavaScript’s asynchronous backbone—without it, we couldn’t handle user interactions, network requests, or promises. 🔄 It’s the invisible engine that ensures our code runs smoothly while waiting for external events. Let’s unpack its inner workings with precision.
Call Stack
The call stack is JavaScript’s active execution queue. Think of it as a stack of function calls where each function creates a “stack frame” (a memory space holding variables, parameters, and execution context). The stack operates on a last-in-first-out (LIFO) principle: the most recently called function runs before older ones.
Here’s how it works in practice:
- When you call a function, a new stack frame is pushed onto the call stack.
- The top frame executes until it completes or hits a
returnstatement. - Once a frame finishes, it’s popped from the stack, freeing memory.
<code class="language-javascript">function printStack() {
<p> console.log('Stack frame:', new Error().stack);</p>
<p>}</p>
<p>function step1() {</p>
<p> printStack();</p>
<p> step2();</p>
<p>}</p>
<p>function step2() {</p>
<p> printStack();</p>
<p>}</p>
<p>step1();</code>
Output:
<code>Stack frame: step1 → step2 → printStack → (eval) <p>Stack frame: step1 → step2 → printStack → (eval)</code>
This example shows:
step1starts first → creates its stack framestep1callsstep2→ pushesstep2onto the stackstep2runs → creates its stack frame- Both frames print their stack traces before popping
Critical insight: The call stack always holds only active function calls. When it’s empty, the event loop checks the task queues (microtasks and macrotasks) to continue execution.
Microtasks vs Macrotasks
The event loop processes tasks in two distinct queues: microtasks and macrotasks. Understanding their order is crucial for writing predictable asynchronous code.
Key Differences
| Feature | Microtasks | Macrotasks |
|---|---|---|
| Queue | Microtask queue (e.g., Promise callbacks) |
Macrotask queue (e.g., setTimeout, setInterval) |
| Execution Order | Before macrotasks (after call stack) | After microtasks (when call stack is empty) |
| Examples | Promise.then(), queueMicrotask(), MutationObserver |
setTimeout(), setInterval(), I/O |
| Priority | Highest (executed immediately after call stack) | Lower (executed when call stack is empty) |
Real-World Example: Promise vs setTimeout
This classic example reveals the microtask-macrotask hierarchy:
<code class="language-javascript">console.log('Start');
<p>Promise.resolve()</p>
<p> .then(() => {</p>
<p> console.log('Microtask: Promise callback');</p>
<p> });</p>
<p>setTimeout(() => {</p>
<p> console.log('Macrotask: setTimeout');</p>
<p>}, 0);</p>
<p>console.log('End');</code>
Output:
<code>Start <p>End</p> <p>Microtask: Promise callback</p> <p>Macrotask: setTimeout</code>
Why this happens:
Start→ logs first (call stack is empty)End→ logs next (call stack is empty)- Microtask queue processes
Promise.then()before checking macrotasks - Macrotask queue runs
setTimeoutlast (after all microtasks)
Advanced Scenario: Multiple Microtasks
Microtasks execute in order (like a queue), not concurrently:
<code class="language-javascript">console.log('1');
<p>queueMicrotask(() => console.log('Microtask 1'));</p>
<p>queueMicrotask(() => console.log('Microtask 2'));</p>
<p>setTimeout(() => console.log('Macrotask'), 0);</code>
Output:
<code>1 <p>Microtask 1</p> <p>Microtask 2</p> <p>Macrotask</code>
This demonstrates:
- Microtasks run sequentially (not in parallel)
- They execute immediately after the call stack clears
- Macrotasks only run after all microtasks complete
Why This Matters
Misunderstanding microtask/macrotask order causes subtle bugs:
- Promise chains run as microtasks → critical for async workflows
queueMicrotask()is used for “immediate” async operations (e.g., DOM updates)setTimeoutwith0always executes after microtasks (not immediately)
Summary
The event loop’s power comes from its dual queues: the call stack (active function calls) and task queues (microtasks and macrotasks). Microtasks—like Promise callbacks—execute before macrotasks (like setTimeout) and in sequence. This precise ordering ensures JavaScript handles asynchronous operations predictably while maintaining responsiveness. Master this hierarchy to write robust, efficient code that works seamlessly with modern frameworks.