CodeWithAbdessamad

Event Loop Deep Dive

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:

  1. When you call a function, a new stack frame is pushed onto the call stack.
  2. The top frame executes until it completes or hits a return statement.
  3. 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:

  • step1 starts first → creates its stack frame
  • step1 calls step2 → pushes step2 onto the stack
  • step2 runs → 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:

  1. Start → logs first (call stack is empty)
  2. End → logs next (call stack is empty)
  3. Microtask queue processes Promise.then() before checking macrotasks
  4. Macrotask queue runs setTimeout last (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)
  • setTimeout with 0 always 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.