Skip to main content

Architecture

Background

Hi there. So the event loop, It’s one of those things that every JavaScript developer has to deal with in one way or another, but it can be a bit confusing to understand at first.

In this doc we will dig into how javascript works under the hood, how it executes our asynchronous javascript code, and in what order (Promise vs setTimeout), how it generates stack trace and much more.

As most developers know, that Javascript is single threaded, means, two statement in javascript can not be executed in parallel. Execution happens line by line, which means each javascript statements are synchronous and blocking. But there is a way to run your code asynchronously, if you use setTimeout() function, a Web API given by browser and Nodejs, which makes sure that your code executes after specified time (in millisecond). Example code:

console.log("Message 1");
setTimeout(() => console.log("Message 2"), 100); // Prints after 100ms
console.log("Message 3");
info
setTimeout

setTimeout takes a callback function as first parameter, and time in millisecond as second parameter.

First, Message 1, Message 2 will wait, and in meantime, Message 3 will be printed, then after 100ms, Message 2 will be printed. This is how javascript works, it is single threaded, and it executes code line by line, but it has a way to run code asynchronously. In this doc, we will dig into how javascript works under the hood, how it executes our asynchronous javascript code. Let's start with how javascript works.

How JavaScript works

Before I dive into the explanation of each topic, I want you to look at this high-level overview that I created, which is an abstraction of how JavaScript interacts with the browser.

Don't worry if you don't know what all the terms mean. I will cover each of them in this section.

img.png

You can see we have the following components:

Let's start with the call stack.

Call Stack

You've probably already heard (many times) that JavaScript is single-threaded. But what does this mean?

JavaScript can do one single thing at a time because it has only one call stack. Call stack is where all your javascript code gets pushed and executed one by one as the interpreter reads your program, and gets popped out once the execution is done. If your statement is asynchronous: setTimeout, ajax(), fetch, promise, or click event, then that code gets forwarded to the browser's Web API

Let's see what is Web API, what it does, and how it works. It is very important to understand how Web API works, as it is the reason why javascript is asynchronous.

Web APIs

Above, I said that JavaScript could only do one thing at a time. While this is true for the JavaScript language itself, you can still do things concurrently in the browser (node js), and this is where Web APIs come into play.

When an asynchronous operation is initiated in JavaScript then it is handled by Web APIs outside the main execution thread (the call stack), and the control is returned to the calling code. For example, there's a Network request (API Call), the browser will start the request and return control to the calling code.

Some of the most common Web APIs are:

  • DOM APIs
  • AJAX (XMLHttpRequest)
  • Timeout (setTimeout, setInterval)
  • Fetch
  • Promises
  • requestAnimationFrame
  • Geolocation
  • Web Workers
  • Web Sockets
  • IndexedDB
  • Audio and Video APIs

Once the request is complete and there's a response from the server, the Web API will add it into the queue (job or callback) - the next phase. Let's see what is the queue.

Callback Queue and Job Queue

The queue is a data structure that follows the FIFO (First In First Out) principle. This means that the first element that was added to the queue will be the first to be removed. Once a process is completed, and it's ready to be added to the queue, it will be added to the end of the queue. There are two types of queues:

  1. Callback Queue
  2. Job Queue

The process will either go to the callback queue or the job queue depending on the type of operation. The difference between the two is that the job queue has a higher priority than the callback queue. This means that the job queue will be executed before the callback queue.

Callback Queue

These are the methods that are added to the callback queue:

  • setTimeout
  • setInterval
  • setImmediate
  • requestAnimationFrame
  • I/O
  • UI rendering

If noticed, all of these methods are either asynchronous or event-based. Their callback functions are added to the callback queue once the browser has completed the operation.

Job Queue

It was introduced in ES6 and is reserved only for new Promise() functionality. So when you use promises in your code, you add .then() method. These thenable part is added to Job Queue once the promise has returned/resolved, and then gets executed.

What is a promise? A quick intro

A promise handles the result of an asynchronous operation such as a network request. It can be in one of three states:

  • pending: the initial state, before the operation has completed
  • fulfilled: meaning that the operation was completed successfully
  • rejected: meaning that the operation failed

Want to learn more about promises? Check out this article.

Quick example

Quick Question now: Check these statements for example, can you predict the sequence of output?

// Defining functions

// Synchronous function
const logSync = (message) => console.log(message);

// Asynchronous function using setTimeout
const logWithTimeout = (message) => setTimeout(() => console.log(message), 0);

// Asynchronous function using Promises
const logWithPromise = async (message) => {
new Promise((resolve) => {
console.log(message);
resolve();
});
};

// Calling functions

// Synchronous function call
logSync("Message no. 1: Sync");

// Asynchronous function call using setTimeout
logWithTimeout("Message no. 2: setTimeout");

// Asynchronous function call using Promises
logWithPromise("Message no. 3: 1st Promise").then(() => {
console.log("Message no. 4: 2nd Promise");
});

// Synchronous function call
logSync("Message no. 5: Sync");

Some of you might answer this:

Output
Message no. 1: Sync
Message no. 5: Sync
Message no. 2: setTimeout
Message no. 3: 1st Promise
Message no. 4: 2nd Promise

Thinking Sync functions are executed first, then the setTimeout as it was called first, then promise callbacks are executed. But the output is actually:

Output
Message no. 1: Sync
Message no. 3: 1st Promise
Message no. 5: Sync
Message no. 4: 2nd Promise
Message no. 2: setTimeout

Why is that? Message no. 3: 1st Promise comes before Message no. 5: Sync because in promises only the then part is async, not the all the code inside the new Promise(). So, Message no. 3: 1st Promise is synchronous, and only Message no. 4: 2nd Promise is async. So, Message no. 3: 1st Promise is executed first, then Message no. 5: Sync is executed, then Message no. 4: 2nd Promise is executed, and finally Message no. 2: setTimeout is executed.

Why Message no. 4: 2nd Promise is executed before Message no. 2: setTimeout?: because Promises are added to Job Queue, and setTimeout is added to Callback Queue. Job Queue has high priority over Callback Queue. So, when the call stack is empty, the event loop first checks the Job Queue, and puts them in the call stack which executes it, and then checks the Callback Queue, and puts them in the call stack which executes it.

Event Loop

The event loop is responsible for continuously monitoring the call stack, job queue, and callback queue. Whenever the call stack is empty, the event loop takes the first task from the job queue and executes it. If the job queue is empty, the event loop checks the callback queue and executes any queued callbacks. This process of checking the call stack, job queue, and callback queue continues in a loop, hence the name "event loop".

If the call stack is currently executing some code, the event loop is blocked and won't add any calls from the queue until the stack is empty again. That's why it's important not to block the call stack by running computation-intensive tasks. Otherwise, the event loop won't be able to execute any queued tasks. This can lead to a so-called "deadlock" where the application is unresponsive. You surely would have experienced this when you were using a website that was loading for a long time and saying something like Please wait, the page is loading or This page is unresponsive.

Microtasks and macrotasks

The event loop is not only responsible for executing the code that's in the callback queue. It also executes the code that's in the job queue. But how does it know which code to execute first?

The event loop uses the concept of macrotasks and microtasks to determine which code to execute first. A macrotask is a task that takes a long time to execute, such as a network request or a setTimeout call. A microtask is a task that can be executed quickly, such as a promise callback. The event loop executes all microtasks before it executes any macrotasks.

The event loop executes the microtasks in the job queue first, then it executes the macrotasks in the callback queue. This means that the event loop will execute all the microtasks before it executes any macrotasks. This is why the promise callbacks are executed before the setTimeout callback. 1_0xDGBNrA1WtfSfYY3FJOdw.gif

Code execution notes

  • Your asynchronous code will run after “Main Stack” is done with all the task execution.
  • That is the good part: Your current statements/functions in the stack will run to completion. Async code can not interrupt them. Once your async code is ready to execute, it will wait for main stack to be empty.
  • That also means that it is not guaranteed that your setTimeout() or any other async code will run exactly after the time that you have specified. That time is the minimum time after which your code will be executed, it can be delayed if Main stack is busy executing existing code, normally it doesn't happen that your async code is delayed for more than 1-2ms waiting for Main stack to be empty.
  • If you use 0ms time in your setTimeout, it won’t run immediately (as you might expect), and it will wait for Call Stack to be empty. So, it will execute after all the synchronous code first. Check this example:
    setTimeout(() => console.log("Message 1"), 0);
    console.log("Message 2");
    In above example, the first output will be Message 2, then Message 1, even though the setTimeout is set to run after 0 millisecond. Once the browser encounters the setTimeout it pops it from Call Stack to WebAPI to Callback Queue, where it waits for Call stack to finish the second console.log, then setTimeout gets back to Call Stack by event loop, and runs the callback of setTimeout.
  • If you are doing too much heavy computation, then it will make the browser unresponsive, because your main thread is blocked and can not process any other task. So user will be unable to do any click on your webpage. That’s when Browser throws Script is taking too much time to execute error, and gives you option to kill the script or wait for it.

Conclusion

The event loop is a crucial part of JavaScript. It's what makes JavaScript asynchronous and non-blocking. It's also what makes it possible to run code in the background without blocking the main thread.