How the JavaScript Event Loop Works

By Mohiuddin Murad
October 18, 2023
JavaScript
Event Loop
Async
Microtask
Macrotask
How the JavaScript Event Loop Works

JavaScript is a single-threaded language, meaning it can only perform one task at a time. However, it handles asynchronous operations like setTimeout or API calls through the Event Loop mechanism.

The Event Loop, in conjunction with the Call Stack, Web APIs, Callback Queue (Macrotask Queue), and Microtask Queue, enables non-blocking, asynchronous behavior in JavaScript.

Core Components:

  1. Call Stack: When a function is invoked, it's pushed onto the Call Stack. When it completes, it's popped off. This follows a Last-In, First-Out (LIFO) principle.
  2. Web APIs: Asynchronous functions like setTimeout, DOM events, or fetch are not handled by the JavaScript engine directly. The browser manages them via Web APIs. The Call Stack offloads these tasks to the Web API and proceeds with other synchronous code.
  3. Callback Queue (Macrotask Queue): Once an asynchronous operation in the Web API is complete (e.g., the timer for setTimeout expires), its callback function is placed in the Callback Queue.
  4. Microtask Queue: Callbacks from Promises (.then(), .catch(), .finally()) and async/await are placed in the Microtask Queue. This queue has a higher priority than the Callback Queue.
  5. Event Loop: The Event Loop's primary job is to monitor the Call Stack. When the Call Stack is empty, it first processes all tasks in the Microtask Queue, moving them one by one to the Call Stack for execution. Only after the Microtask Queue is completely empty does it take the first task from the Callback Queue (Macrotask) and push it to the Call Stack.

Example:

Consider the output of the following code snippet:

console.log('Start');

setTimeout(() => {
  console.log('Timeout (Macrotask)');
}, 0);

Promise.resolve().then(() => {
  console.log('Promise (Microtask)');
});

console.log('End');

Output:

Start
End
Promise (Microtask)
Timeout (Macrotask)

Explanation:

  1. console.log('Start') is pushed to the Call Stack and executed immediately.
  2. setTimeout is sent to the Web API. Its callback is moved to the Callback Queue after 0 milliseconds.
  3. The .then() callback from Promise.resolve() is moved to the Microtask Queue.
  4. console.log('End') is pushed to the Call Stack and executed.
  5. Once the Call Stack is empty, the Event Loop processes the Microtask Queue, executing 'Promise (Microtask)'.
  6. After the Microtask Queue is empty, the Event Loop processes the Callback Queue, executing 'Timeout (Macrotask)'.

This mechanism allows JavaScript to handle asynchronous operations efficiently without blocking the main thread.