JavaScript Event Loop

Tiago Farias
Life at Apollo Division
5 min readJan 17, 2024

You may have already dealt with asynchronous programming using JavaScript, but do you know how it works? Or, you may have noticed that a clock implemented with setTimeout is not timing correctly/accurately, but why not?

In this article, I’ll explain how JavaScript’s Event Loop orchestrates the asynchronous code execution in web browsers. It can be not very clear the first time, so to help you, I created animations about this concept and I’ll use them to explain.

The Event Loop is something you might handle every day without knowing, it takes action every time you need to run asynchronous code. So, you as a good software engineer should understand how it works to improve your implementations and for better debugging.

As you may already know JavaScript is single-threaded, meaning only one task can run at a time. So, as the JavaScript runs on the browser’s main thread by default, if your website is running a task that takes too much time to finish, the entire UI is frozen! Leading to user frustration, no one wants to wait for an unresponsive website.

To address this issue, the browser provides us with some features that are not provided by the JavaScript engine itself, a Web API containing HTTP Requests, Pointer Events, setTimeout, and a lot more. The execution of these features handled by the Event Loop, ensures that JavaScript execution remains non-blocking, providing a responsive user experience.

What happens when sync functions are invoked?

Before we jump into the Event Loop, let’s understand how synchronous execution works in JavaScript. An invoked function forms a frame on the top of the Call Stack. The Call Stack is a data structure that follows the Last In, First Out (LIFO) principle. As soon as the function finishes its execution, the top frame element is popped out from the stack, and the next function in line is executed. This process continues until the Call Stack is empty.

The hello function returns a setTimeout which is an async function provided by the Web API, it enables us to run a code after some time (in ms) without pausing the main thread execution. The arrow function () => console.log(“Hello World!”) passed as a callback to the setTimeout is sent to the Web API and the setTimeout and hello frame elements get popped off from the Call Stack.

After the time set in the second argument of the setTimeout has passed (500ms), the arrow function is not pushed to the Call Stack! instead, the Web API pushes the function to something called the Message Queue.

Message Queue, the async operations holder

While the Call Stack handles the synchronous tasks in the form of frames, the Message Queue handles the asynchronous tasks in the form of messages. Each message has an associated function to be called. Therefore, asynchronous tasks are pushed to the Message Queue once they’re ready to be executed.

Here is the interesting part: The Event Loop is constantly monitoring the Call Stack, and if it’s empty, it moves the task from the Message Queue to the Call Stack for execution, but if not, the task will have to wait for other messages or frames to be processed. For this reason, the second argument of the setTimeout function indicates a minimum time, not a guaranteed time.

This explains why the clock implementation is not accurate! Even though we set 1000 milliseconds as the second argument in the setTimeout, it doesn’t guarantee the callback function will be called precisely after 1000 milliseconds. Instead, it indicates that the callback function will be called after a minimum delay of 1000 milliseconds. The execution depends on the number of waiting tasks in the queue.

Tasks and Microtasks

One important thing to know about the Message Queue is that it is divided into two types of queues: the Task Queue and the Microtask Queue. The Task Queue holds tasks that are pushed onto the queue when asynchronous operations are completed. This includes setTimeout, I/O operations, events, rendering, and so on. On the other hand, the Microtask Queue holds microtasks, which are tasks with higher priority, they are executed after the current task, and before the next regular task. The most common microtasks examples are Promises, MutationObserver, process.nextTick (in Node.js), and queueMicrotask.

The Event Loop flow visualization

The Event Loop, the Call Stack, and the Message Queue together compose JavaScript’s concurrency model. The developer who is comfortable with the event loop concept can take the maximum leverage of asynchronous programming, implementing non-blocking and efficient applications, and most importantly, can understand where the errors and behaviors come from. I hope the gifs helped you understand the flow of the Event Loop.

Thank you for reading!

Sources:

We are ACTUM Digital and this piece was written by Tiago Farias, Front End Developer of Apollo Division. Feel free to get in touch.

--

--