DEV Community

Cover image for JavaScript Event Loop: The Complete Guide to Understanding Asynchronous JavaScript
Mr Shuvo
Mr Shuvo

Posted on

JavaScript Event Loop: The Complete Guide to Understanding Asynchronous JavaScript

Introduction

If you've been working with JavaScript for a while, you've probably heard that JavaScript is single-threaded. At the same time, you've also seen JavaScript handle API requests, timers, user interactions, file operations, and many asynchronous tasks seemingly at the same time.

This raises an important question:

How can a single-threaded language perform asynchronous operations without blocking the entire application?

The answer lies in one of the most important concepts in JavaScript: The Event Loop.

Understanding the Event Loop is essential for becoming a proficient JavaScript developer. It helps explain the behavior of Promises, async/await, setTimeout, API calls, and many interview questions that often confuse developers.

In this article, we'll explore the Event Loop from the ground up and understand exactly how JavaScript executes code behind the scenes.


Understanding JavaScript's Single-Threaded Nature

JavaScript executes code using a single thread. This means it can only perform one operation at a time.

Consider the following example:

console.log("Task 1");
console.log("Task 2");
console.log("Task 3");
Enter fullscreen mode Exit fullscreen mode

Output:

Task 1
Task 2
Task 3
Enter fullscreen mode Exit fullscreen mode

The code executes line by line in a synchronous manner.

Now imagine a situation where a task takes several seconds to complete:

console.log("Start");

function heavyTask() {
  for (let i = 0; i < 10000000000; i++) {}
}

heavyTask();

console.log("End");
Enter fullscreen mode Exit fullscreen mode

The browser must wait for the entire loop to finish before executing the next line.

This creates a problem. If JavaScript waited for every operation to finish before moving forward, modern web applications would feel slow and unresponsive.

To solve this problem, JavaScript relies on asynchronous programming powered by the Event Loop.


What Exactly Is the Event Loop?

The Event Loop is a mechanism that continuously monitors the Call Stack and various task queues, ensuring that asynchronous operations are executed when JavaScript is ready.

Simply put:

The Event Loop allows JavaScript to perform non-blocking asynchronous operations even though it runs on a single thread.

Without the Event Loop, features like API requests, timers, and user interactions would block the entire application.


The JavaScript Runtime Environment

To understand the Event Loop properly, we need to understand the components involved in the JavaScript runtime environment.

These components include:

  • Call Stack
  • Web APIs
  • Callback Queue (Macrotask Queue)
  • Microtask Queue
  • Event Loop

A simplified architecture looks like this:

┌─────────────────┐
│    Call Stack   │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│     Web APIs    │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│ Callback Queue  │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│   Event Loop    │
└─────────────────┘
Enter fullscreen mode Exit fullscreen mode

Let's examine each component individually.


The Call Stack

The Call Stack is where JavaScript keeps track of function execution.

Whenever a function is called, it gets pushed onto the stack. Once execution finishes, it gets removed.

Example:

function first() {
  second();
}

function second() {
  third();
}

function third() {
  console.log("Hello World");
}

first();
Enter fullscreen mode Exit fullscreen mode

Execution flow:

Call Stack

first()
second()
third()
console.log()
Enter fullscreen mode Exit fullscreen mode

After execution:

Call Stack Empty
Enter fullscreen mode Exit fullscreen mode

The Call Stack follows the Last In, First Out (LIFO) principle.

The most recently added function executes first.


Web APIs

Web APIs are not part of the JavaScript language itself.

They are provided by the browser (or Node.js runtime) and allow JavaScript to perform asynchronous operations.

Examples include:

  • setTimeout
  • setInterval
  • Fetch API
  • DOM Events
  • Geolocation API
  • XMLHttpRequest

Consider:

setTimeout(() => {
  console.log("Hello");
}, 2000);
Enter fullscreen mode Exit fullscreen mode

What happens internally?

  1. JavaScript encounters setTimeout.
  2. The timer is registered with the browser.
  3. JavaScript immediately continues executing other code.
  4. After two seconds, the callback becomes eligible for execution.
  5. The callback enters a queue and waits for the Event Loop.

This is why JavaScript doesn't stop and wait for the timer.


Callback Queue (Macrotask Queue)

The Callback Queue stores completed asynchronous callbacks that are ready to execute.

Example:

setTimeout(() => {
  console.log("A");
}, 1000);

setTimeout(() => {
  console.log("B");
}, 500);
Enter fullscreen mode Exit fullscreen mode

Output:

B
A
Enter fullscreen mode Exit fullscreen mode

The callback associated with "B" finishes first and enters the queue before "A".

The queue follows a FIFO structure:

First In
First Out
Enter fullscreen mode Exit fullscreen mode

However, entering the queue does not mean immediate execution.

The callback must still wait until the Call Stack becomes empty.


The Event Loop in Action

The Event Loop continuously checks whether the Call Stack is empty.

When the stack becomes empty, the Event Loop moves pending tasks from queues into the Call Stack for execution.

Conceptually:

while (true) {
  if (callStack.isEmpty()) {
    executeNextTask();
  }
}
Enter fullscreen mode Exit fullscreen mode

This is a simplified representation, but it captures the fundamental idea.

The Event Loop acts as a bridge between the Call Stack and task queues.


Understanding setTimeout()

Let's examine a classic interview question.

console.log("Start");

setTimeout(() => {
  console.log("Timer");
}, 0);

console.log("End");
Enter fullscreen mode Exit fullscreen mode

Many developers expect:

Start
Timer
End
Enter fullscreen mode Exit fullscreen mode

Actual output:

Start
End
Timer
Enter fullscreen mode Exit fullscreen mode

Why?

Even though the timeout value is zero milliseconds, the callback cannot execute immediately.

The process is:

  1. "Start" is logged.
  2. Timer is registered.
  3. "End" is logged.
  4. Call Stack becomes empty.
  5. Event Loop moves callback to Call Stack.
  6. "Timer" is logged.

The delay specifies the minimum wait time, not the exact execution time.


Microtask Queue

The Event Loop doesn't work with only one queue.

Modern JavaScript introduces another important queue:

Microtask Queue

Microtasks have higher priority than normal callbacks.

Examples of microtasks:

  • Promise.then()
  • Promise.catch()
  • Promise.finally()
  • queueMicrotask()
  • MutationObserver

Example:

console.log("Start");

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

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

console.log("End");
Enter fullscreen mode Exit fullscreen mode

Output:

Start
End
Promise
Timeout
Enter fullscreen mode Exit fullscreen mode

This surprises many developers.

The reason is that Promise callbacks are placed in the Microtask Queue.


Why Do Promises Execute Before setTimeout?

Let's analyze the previous example step by step.

console.log("Start");
Enter fullscreen mode Exit fullscreen mode

Output:

Start
Enter fullscreen mode Exit fullscreen mode

Next:

setTimeout(...)
Enter fullscreen mode Exit fullscreen mode

The callback goes to Web APIs.

Then:

Promise.resolve().then(...)
Enter fullscreen mode Exit fullscreen mode

The Promise callback goes to the Microtask Queue.

Then:

console.log("End");
Enter fullscreen mode Exit fullscreen mode

Output:

End
Enter fullscreen mode Exit fullscreen mode

At this point, the Call Stack becomes empty.

The Event Loop checks:

  1. Microtask Queue
  2. Macrotask Queue

Since microtasks have priority, the Promise callback executes first.

Output:

Promise
Enter fullscreen mode Exit fullscreen mode

Only afterward does the timeout callback execute.

Output:

Timeout
Enter fullscreen mode Exit fullscreen mode

Event Loop Priority Rules

The Event Loop follows a specific priority order:

1. Execute synchronous code.
2. Empty the Microtask Queue.
3. Execute one Macrotask.
4. Repeat.
Enter fullscreen mode Exit fullscreen mode

Visual representation:

Call Stack
     ↓
Microtask Queue
     ↓
Macrotask Queue
Enter fullscreen mode Exit fullscreen mode

This rule explains many seemingly strange execution orders in JavaScript.


Complex Example

Consider:

console.log("1");

setTimeout(() => {
  console.log("2");
}, 0);

Promise.resolve().then(() => {
  console.log("3");
});

console.log("4");
Enter fullscreen mode Exit fullscreen mode

Output:

1
4
3
2
Enter fullscreen mode Exit fullscreen mode

Execution breakdown:

  • "1" executes immediately.
  • Timer callback enters Web APIs.
  • Promise callback enters Microtask Queue.
  • "4" executes.
  • Stack becomes empty.
  • Microtask Queue executes.
  • Promise logs "3".
  • Macrotask Queue executes.
  • Timer logs "2".

Nested Microtasks

Microtasks can generate additional microtasks.

Example:

Promise.resolve().then(() => {
  console.log("A");

  Promise.resolve().then(() => {
    console.log("B");
  });
});
Enter fullscreen mode Exit fullscreen mode

Output:

A
B
Enter fullscreen mode Exit fullscreen mode

The Event Loop keeps processing microtasks until the Microtask Queue becomes completely empty.

Only then does it move to macrotasks.


Async/Await and the Event Loop

Async/Await is built on top of Promises.

Example:

async function demo() {
  console.log("1");

  await Promise.resolve();

  console.log("2");
}

demo();

console.log("3");
Enter fullscreen mode Exit fullscreen mode

Output:

1
3
2
Enter fullscreen mode Exit fullscreen mode

What happens?

The await keyword pauses the function.

The remaining code after await gets scheduled as a microtask.

Execution order:

  1. "1" executes.
  2. Function pauses.
  3. "3" executes.
  4. Microtask resumes function.
  5. "2" executes.

Understanding this behavior becomes much easier once you understand the Event Loop.


Browser Events and the Event Loop

Consider:

button.addEventListener("click", () => {
  console.log("Button Clicked");
});
Enter fullscreen mode Exit fullscreen mode

When the user clicks:

  1. Browser detects the event.
  2. Callback enters a queue.
  3. Event Loop waits for an empty stack.
  4. Callback executes.

This mechanism allows browsers to remain responsive while handling thousands of interactions.


Event Loop in Node.js

Node.js also uses an Event Loop, but its implementation is more sophisticated.

Node's Event Loop consists of several phases:

Timers
Pending Callbacks
Idle
Poll
Check
Close Callbacks
Enter fullscreen mode Exit fullscreen mode

Node.js additionally provides:

process.nextTick()
Enter fullscreen mode Exit fullscreen mode

and

setImmediate()
Enter fullscreen mode Exit fullscreen mode

which introduce additional scheduling behavior beyond the browser environment.

Understanding these mechanisms becomes important when building high-performance backend applications.


Common Interview Question

Predict the output:

console.log("A");

setTimeout(() => {
  console.log("B");
}, 0);

Promise.resolve().then(() => {
  console.log("C");
});

console.log("D");
Enter fullscreen mode Exit fullscreen mode

Output:

A
D
C
B
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • A executes immediately.
  • Timer enters Macrotask Queue.
  • Promise enters Microtask Queue.
  • D executes.
  • Stack becomes empty.
  • Microtask executes (C).
  • Macrotask executes (B).

Another Tricky Example

console.log("1");

setTimeout(() => {
  console.log("2");
}, 0);

Promise.resolve()
  .then(() => {
    console.log("3");
  })
  .then(() => {
    console.log("4");
  });

console.log("5");
Enter fullscreen mode Exit fullscreen mode

Output:

1
5
3
4
2
Enter fullscreen mode Exit fullscreen mode

Both Promise callbacks are microtasks, so they execute before the timer callback.


A Simplified Event Loop Algorithm

The Event Loop can be summarized as follows:

1. Execute synchronous code.
2. Send asynchronous operations to Web APIs.
3. Move completed callbacks into queues.
4. Wait until Call Stack becomes empty.
5. Execute all Microtasks.
6. Execute one Macrotask.
7. Repeat forever.
Enter fullscreen mode Exit fullscreen mode

Although real JavaScript engines are significantly more complex, this model explains most practical scenarios.


Key Takeaways

  • JavaScript is single-threaded.
  • The Call Stack executes synchronous code.
  • Browsers and runtimes provide Web APIs.
  • Asynchronous callbacks wait in task queues.
  • The Event Loop coordinates execution.
  • Promises use the Microtask Queue.
  • Microtasks always have higher priority than Macrotasks.
  • Async/Await relies on Promises and microtasks.
  • Understanding the Event Loop is crucial for writing efficient asynchronous JavaScript.

Conclusion

The JavaScript Event Loop is one of the most fundamental concepts in modern web development. While JavaScript executes code on a single thread, the Event Loop enables it to handle asynchronous operations efficiently without blocking the application.

Once you understand how the Call Stack, Web APIs, Microtask Queue, Macrotask Queue, and Event Loop work together, concepts such as Promises, async/await, timers, and event handlers become much easier to reason about.

Mastering the Event Loop not only helps you write better code but also prepares you for advanced JavaScript topics, performance optimization, and technical interviews. Every serious JavaScript developer should invest time in understanding this mechanism because it sits at the heart of how JavaScript actually works.

Top comments (0)