Understanding the Event Loop: The Heart of Asynchronous JavaScript
## 📋 Table of Contents
1. Introduction
2. Step-by-Step Explanation of the JavaScript Event Loop Execution
3. Data Structures: Call Stack and Event Queue
4. Key Takeaways
🔗 **Live Demo** : Click Here
🎥 **Demo Video** :
## Introduction
JavaScript is a **single-threaded** language, meaning it can only execute one task at a time on the main thread. However, this doesn't mean JavaScript can't handle multiple tasks simultaneously. The key to understanding this behavior lies in the **Event Loop**.
The Event Loop allows JavaScript to perform asynchronous operations, such as waiting for a network request or timer, without blocking the main thread. By offloading tasks to be executed later, JavaScript creates the illusion of parallel execution while still maintaining its single-threaded nature.
In this article, we’ll take a closer look at how the Event Loop manages both **synchronous** and **asynchronous** tasks. We’ll walk through how JavaScript handles operations, manages callbacks, and uses the Event Loop to ensure that asynchronous tasks don't block the execution of synchronous code.
To help visualize this process, we provide a **series of visualizations** that illustrate each step of how JavaScript handles tasks. These visualizations are based on a specific code example, which is shown throughout the images to simplify the explanation. Each visualization is accompanied by a **description** to clarify what's happening at each step. This approach helps us see how tasks move through the **Call Stack** , the **Event Queue** , and how the **Event Loop** orchestrates everything.
## Data Structures: Call Stack and Event Queue
In this article, we’ll also briefly touch on the two main data structures that help JavaScript manage synchronous and asynchronous tasks:
🍽️🍽️🍽️ **Call Stack** : The _Call Stack_ is where JavaScript keeps track of **synchronous** functions that are executing. It works like a stack of plates in the kitchen sink — as each plate gets added, it’s placed on top of the stack. When the sink gets full, the plate that is **added last is the first to be removed**. Similarly, when a function is called, it’s added on top of the stack, and once it finishes executing, it’s removed from the top.
☕🚶♂️🚶♀️ **Event Queue** : The _Event Queue_ holds **asynchronous** tasks waiting to be processed. It works like a line at a coffee shop. The first person in line is the first to get coffee, meaning the **first task added in the queue will be the first one to be processed** once the _Call Stack_ is clear.
## Step-by-Step Explanation of the JavaScript Event Loop Execution
For better visualization, all synchronous execution contexts are pushed onto the call stack, ready to be executed.
🚀 `console.log('Start');` is executed immediately, producing `"Start"` as output.
⏸️ The _Event Loop_ is **paused** while synchronous code runs in the main thread.
⏳ The `setTimeout(() => { zeroSecondsLater(); }, 0);` function is **asynchronous**.
🔄 The JavaScript engine **delegates** it to the _Browser API_.
📩 Since the delay is 0ms, the _Browser API_ processes the request immediately and moves the `zeroSecondsLater callback` to the _Event Queue_.
🔄 `setTimeout(() => { console.log('3 seconds later'); }, 3000);` is **delegated** to the _Browser API_ , which starts a 3-second timer.
⏳ Similarly, `setTimeout(() => { console.log('4 seconds later'); }, 4000);` is **delegated** to the _Browser API_ , which starts a 4-second timer.
⏩ Meanwhile, **synchronous code continues executing in the main thread without waiting for these timers**.
🛑 Important: The **Call Stack executes only synchronous functions** and must complete all synchronous tasks before handling anything else. The _Event Loop_ does not move callbacks from the _Event Queue_ to the _Call Stack_ until all synchronous code has finished executing.
🚀 `console.log('End');` is executed immediately, producing `"End"` as output.
👀 At this point, the _Call Stack_ is empty, **signaling** the _Event Loop_ to check the event queue.
🎧 The _Event Loop_ waits for the _Call Stack_ to become empty and then moves the **first** callback from the _Event Queue_ to the _Call Stack_ for execution.
🔁 The `zeroSecondsLater();` callback is moved from the event queue to the call stack.
📩 The _Browser API_ completes the 3-second timer and moves `console.log('3 seconds later');` to the _Event Queue_.
📩 One more second has passed so the _Browser API_ moves `console.log('4 seconds later');` to the _Event Queue_ , placing it after the `console.log('3 seconds later');` callback.
🔄 The `zeroSecondsLater();` callback invokes `oneSecondLater();`.
🔄 `oneSecondLater();` invokes `console.log`.
💬 `"1 second later"` is printed in the console.
🔄 Then, `zeroSecondsLater();` invokes `twoSecondsLater();`.
🔄 `twoSecondsLater();` invokes `console.log`.
💬 `"2 seconds later"` is printed in the console. All the synchronous code has been executed and the callback function `zeroSecondsLater()` completes its execution. At this point, the _Call Stack_ becomes empty, signaling the _Event Loop_ to begin processing callbacks from the _Event Queue_.
👀 The _Event Loop_ moves `console.log('3 seconds later');` from the _Event Queue_ to the _Call Stack_.
🚀 The function executes, producing `"3 seconds later"` in the console.
👀 Again, the _Call Stack_ is empty, allowing the _Event Loop_ to move `console.log('4 seconds later');` from the _Event Queue_ to the _Call Stack_.
🚀 The function executes, producing `"4 seconds later"` in the console.
## Key Takeaways
✅ **Only synchronous code is executed in the Call Stack** (Execution Stack) on the **main thread**. The _Event Loop_ does not move callbacks from the _Event Queue_ to the _Call Stack_ until all synchronous code has finished executing
✅ Asynchronous functions (e.g., setTimeout) are **delegated** to the _Browser API_ , which processes them in **parallel** while synchronous code continues.
✅ Callbacks enter the _Event Queue_ based on when they were **processed** by the _Browser API_ , not necessarily in the order they were requested.
✅ The _Event Loop_ waits for the _Call Stack_ to be **empty** before moving a callback from the _Event Queue_ to the _Call Stack_.
Thank you for reading!
I would be grateful to understand your opinion.