Skip to content
@aploon

Understanding the javascript event loop

  • #javascript
  • #runtime
  • #fundamentals

Understanding the JavaScript Event Loop: The Definitive Guide

JavaScript is a single-threaded language. This means it has only one call stack (Call Stack) and can execute only one piece of code at a time.

Yet, modern JavaScript applications smoothly handle network requests, user clicks, and complex animations simultaneously without freezing the UI. This architectural feat relies entirely on a central mechanism: the Event Loop.


1. The 4 Core Components of the Concurrency Model

To understand how asynchronous code is orchestrated, you need to visualize four components working together behind the scenes:

  • The Call Stack: This is the primary engine. Functions are pushed onto the stack when called and popped off once they return a value. It operates on a LIFO (Last In, First Out) basis.
  • Web APIs (or Runtime Environment): Provided by the browser (or Node.js), these handle heavy background tasks like fetch(), setTimeout(), and DOM event listeners.
  • The Microtask Queue: A high-priority queue that stores callbacks from promises (.then(), catch(), finally()) and async/await operations.
  • The Task Queue (or Macro-task Queue): A standard-priority queue that stores callbacks from DOM events, setTimeout(), and setInterval().

2. How the Event Loop Works

The Event Loop is a continuous loop whose sole job is to monitor the Call Stack. It follows a strict, golden rule:

  1. Execute Synchronous Code: The Event Loop waits until the Call Stack is completely empty.
  2. Clear the Microtask Queue: As soon as the stack clears, the Event Loop checks the Microtask Queue and executes all pending microtasks sequentially until the queue is entirely empty.
  3. Process One Macro-task: Once the Microtask Queue is completely clear, the Event Loop picks only one macro-task from the Task Queue and pushes it to the Call Stack for execution.
  4. Repeat: The cycle starts all over again.

3. Practical Example and Execution Order

Let's break down the execution flow of this classic code snippet:

console.log('1: Start of script');
 
setTimeout(() => {
  console.log('2: setTimeout callback (Macro-task)');
}, 0);
 
Promise.resolve().then(() => {
  console.log('3: Promise callback (Micro-task)');
});
 
console.log('4: End of script');

Console Output:

1: Start of script
4: End of script
3: Promise callback (Micro-task)
2: setTimeout callback (Macro-task)

Why this specific order?

  1. Messages 1 and 4 are synchronous. They are pushed directly to the Call Stack and execute immediately.
  2. The setTimeout delegates its countdown to the Web API, which instantly moves its callback to the Task Queue (macro-task).
  3. The resolved promise places its .then() callback into the Microtask Queue.
  4. Once the synchronous code finishes (the Call Stack becomes empty), the Event Loop processes the Microtask Queue first, printing message 3.
  5. Finally, the Event Loop picks up the remaining macro-task, printing message 2.

4. Why This Matters for Developers

  • Preventing Main Thread Blocking: If you run a heavy mathematical computation synchronously, the Call Stack stays occupied. The Event Loop freezes, causing the user interface to hang or lag.
  • Predictable Asynchronous Code: Understanding the Event Loop allows you to confidently predict code execution order and prevent race conditions or bugs related to data timing.
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                  CALL STACK                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                     โ”‚ (When empty)
                                     โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚             MICROTASK QUEUE (Promises)                                   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                     โ”‚ (Once completely empty)
                                     โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚             TASK QUEUE (setTimeout, DOM)                                 โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

In Summary

JavaScript simulates parallelism through the Event Loop. By delegating time-consuming tasks to the runtime environment and prioritizing execution using separate queues (Micro vs. Macro), JavaScript remains exceptionally fast, responsive, and non-blocking.