Javascript Event Loop

Mohammed owaisMohammed owais
5 min read

Why Your JavaScript Runs Out of Order: The Event Loop Explained

If you're working with JavaScript, you've likely encountered a situation like this. You have the following code:

setTimeout(() => console.log("Timeout!"), 0);
Promise.resolve().then(() => console.log("Promise!"));
console.log("End");

Logically, you might expect the output to be End, Timeout!, Promise!. However, when you run it, the console shows:

End
Promise!
Timeout!

This isn't a bug; it's a fundamental part of how JavaScript handles asynchronous operations. While most of your code runs top-to-bottom, asynchronous tasks follow a different set of rules. This behavior is managed by a system involving the Call Stack, Web APIs, two distinct Queues, and the Event Loop.

Let's break down how this system works:

1. The Global Execution Context and The Call Stack

When you run a JavaScript file, the engine first creates a Global Execution Context (GEC). This is the base environment where your code runs. The process has two phases:

  • Creation Phase: The engine quickly scans the code for all variable and function declarations and allocates memory for them. This process is often called hoisting.

  • Execution Phase: The engine runs your code line by line.

To manage this execution, JavaScript uses a Call Stack. You can think of it as a list of tasks to be done. When a function is called, it's added (pushed) to the top of the stack. When it finishes, it's removed (popped) from the stack. The engine is always working on the task at the very top of the stack.

2. Web APIs

When the JavaScript engine encounters an asynchronous operation like setTimeout, a fetch request, or a DOM event listener, it doesn’t handle it directly. Waiting for these tasks to complete would block all other code from running, freezing the user interface.

Instead, the engine offloads these tasks to Web APIs provided by the browser (or the Node.js runtime). The browser can handle things like timers or network requests in the background. The JavaScript engine is then free to continue executing the rest of your synchronous code.

Once the Web API finishes its work (e.g., the timer runs out), it doesn't just interrupt your code. It places the callback function associated with the task into a queue.

3. The Task Queue and The Microtask Queue

This is a critical part of the process. There are two primary queues where callbacks wait for their turn to run:

  1. Task Queue (or "Macrotask Queue"): This is where callbacks from older APIs like setTimeout, setInterval, and DOM events are placed

  2. Microtask Queue: This is a newer, higher-priority queue. It's used for callbacks from modern asynchronous features, most notably Promises (e.g., .then(), .catch(), .finally()) and async/await

The key rule to remember is that the Microtask Queue always has priority. The Event Loop will always process every task in the Microtask Queue before it considers processing a single task from the Task Queue.

4. The Event Loop

The Event Loop has one simple, continuous job: to monitor the Call Stack and the queues. It follows this cycle:

  1. Check if the Call Stack is empty

  2. If it is, check the Microtask Queue. If there are tasks waiting, take the first one, push it onto the Call Stack, and let it run. The loop will continue doing this until the Microtask Queue is completely empty

  3. Only when the Call Stack and the Microtask Queue are both empty will the Event Loop check the Task Queue. If a task is waiting, it will take the oldest one, push it onto the Call Stack, and let it run

This cycle repeats, ensuring tasks are executed in the correct order of priority.

5. Putting It All Together

Let's revisit our original code and trace its execution with this system in mind.

console.log("Global start");
setTimeout(() => console.log("setTimeout callback"), 0);
Promise.resolve().then(() => console.log("Promise callback"));
function greet() {
  console.log("Hello from greet!");
}
greet();
console.log("Global end");

Here's a detailed breakdown of the execution flow:

  • JS engine goes through the whole code, allocate memory for greet()

  • Push the Global execution context on the stack

  • console.log("Global start") is executed. Output: Global start

  • setTimeout is encountered. The engine hands it off to the Web API. The timer starts (for 0ms)

  • Promise.resolve().then(...) is encountered. The promise resolves immediately, and its callback is placed in the Microtask Queue

  • The greet() function is called and executed. Output: Hello from greet!

  • console.log("Global end") is executed. Output: Global end

  • The main script has now finished. The synchronous code is done, and the Call Stack is empty

  • The Event Loop checks the Microtask Queue. It finds the promise callback, moves it to the Call Stack, and executes it. Output: Promise callback

  • The Event Loop checks the Microtask Queue again. It's empty. Now it checks the Task Queue. By this time, the 0ms timer has completed, and the Web API has placed the setTimeout callback in the Task Queue. The Event Loop moves it to the Call Stack and executes it. Output: setTimeout callback.

This explains the final output:

Global start
Hello from greet!
Global end
Promise callback
setTimeout callback

A Practical Example

This same logic applies to everyday tasks like fetching data from an API.

console.log("Requesting user data...");
fetch('https://api.example.com/users/1')
  .then(response => response.json())
  .then(user => console.log("User data:", user));
console.log("Doing other tasks while waiting...");
  1. The first console.log runs

  2. fetch is called and handed off to the browser's Web API to manage the network request in the background

  3. The final console.log runs immediately, without waiting for the fetch to complete

  4. When the network request succeeds, its .then() callback is placed in the Microtask Queue, giving it priority to run as soon as the main thread is free

Understanding this sequence synchronous code first, then microtasks, then tasks is key to understanding JavaScript better.

0
Subscribe to my newsletter

Read articles from Mohammed owais directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Mohammed owais
Mohammed owais