How the JavaScript Event Loop Works

A look at how the Call Stack, Web APIs, and Callback Queue all work together

Kyle Farmer
JavaScript in Plain English

--

Loupe by Philip Roberts will be used for demonstrations

The Event Loop is the system that JavaScript uses in the browser to handle the execution of multiple pieces of code. It consists of the Call Stack, Web APIs, and the Callback Queue. It is responsible for giving JavaScript its asynchronous programming ability.

JavaScript is a single-threaded, asynchronous language with a concurrency model that is based on a “run to completion” event loop… wait, what? Read on, and this sentence will hopefully make sense by the time you finish.

Prerequisite knowledge: basic JS functions, console.logs, and Web APIs like setTimeout and addEventListener.

Let’s Break it Down

Single-Threaded: When we say that JavaScript is a single-threaded language, this means that it can only “process” one command at a time, from top to bottom.

Asynchronous: Wait, how can JavaScript be asynchronous and single-threaded? Well, technically the browser and its Web APIs are what give JavaScript its asynchronous powers; the language by itself actually doesn’t have this capability. Asynchronous functions allow code to run at a later time (in our Callback Queue) that would otherwise slow down the loading of our web app.

“Run to Completion”: This tells us that each JavaScript function will run in its entirety before the next one is executed. If a function takes a long time to run, the web application will basically pause while it waits for it to complete and move on to the next one. This is known as “blocking the stack”. The browser and its asynchronous Web APIs will help us with this problem later.

Call Stack: This is the stack (yes, just like the stack data structure) where the code is executed one step at a time. JavaScript functions are added (push()) and removed (pop()) to the stack in a LIFO (Last In, First Out) order. There is only one Call Stack in JavaScript, this is what makes it single-threaded. A multi-threaded language like Java can have many stacks running code at the same time. It is important to understand that the page being rendered in the browser will not be interactive until the Call Stack has finished running all of the synchronous JavaScript.

Web APIs: These asynchronous calls (i.e. addEventListener, setTimeout) are provided by the browser to give additional functionality to the JavaScript engine. After a Web API is pushed onto the Call Stack, it is then sent to a separate Web API container and once an action is triggered (i.e. a “click” event, a timer ending, an HTTP request) the callback function is added to the Callback Queue.

Callback Queue: This is a queue data structure that adds the callback functions from the Web APIs in a FIFO (First In, First Out) order. After the Call Stack is through running all of the synchronous JavaScript, the Callback Queue is dequeued onto the Call Stack one function at a time and then executed.

Synchronous Call Stack Examples

Let’s take a look at some synchronous code being run. Only the Call Stack will be utilized in this example:

Loupe

As you see, we begin at the top, where console.log(“START”) is sent first to the Call Stack. It is removed after it runs. We then move down to the sayHi function being invoked. The function is added to the stack, and then the console.log inside of it is added on top of the function and executed. Each is removed in the FIFO order and the same step is repeated with sayBye(). The last code run is console.log(“END”), then removed, and our stack is empty again as it has completed running the code.

Let’s add some more complexity (but still no asynchronous Web APIs) to better understand how the call stack runs code. What happens if a function is called inside of another function? Take a look:

Loupe

The answer is YES. Functions that are called inside of other functions will be run in the stack, and will then run to completion before finishing the code in the function from which they were called. Notice how the sayWhatsUp function literally stacks, in the call stack, on top of the sayHi function.

Asynchronous Web API Examples

Let’s look at the setTimeout Web API first. As you may know, setTimeout takes two arguments, a callback function, and a time (in milliseconds) until the execution of the callback. Let’s see what happens when we pass our sayHi function in as the callback of setTimeout:

Loupe

Our setTimeout function is sent to the Web APIs container after it is processed in the Call Stack. After 3000 ms (3 seconds), the callback sayHi() is sent to the Callback Queue where it waits for the end of the synchronous code to finish running and is then dequeued and run in the Call Stack. Something to note: the 3000 ms time that we set is the minimum amount of time it will be before the callback is run. If our synchronous code is still being executed in the Call Stack when the 3000 ms is over, the callback will still have to wait until all of the code has been completed before it will be sent to the Call Stack. Let’s set the setTimeout time to 0 and see this in action:

Loupe

This clearly demonstrates how the Callback Queue isn’t run until our Call Stack is empty, even if the time has passed on our setTimeout function.

Let’s look at an example of how “blocking” occurs in the Call Stack and how Web APIs can help solve the problem. Let’s make our sayBye function say “bye” 3 times. We’ll add a button with a click event listener that logs “I was clicked” whenever it is clicked. We will call the sayBye3Times function before we add the event listener:

Loupe

As you can see, the Call Stack is being blocked by all the console.logs being called in sayBye3Times, and therefore our click listener isn’t instantiated and our clicks are ignored. Let’s look at how we can improve this with the asynchronous Web API setTimeout:

By putting the sayBye3Times function inside the setTimeout function and therefore making it asynchronous, our event listener is no longer blocked from the Call Stack and can be sent to the Web APIs container to be instantiated. Now our clicks can be registered even while the sayBye3Times function is being executed.

Conclusion

Hopefully, this makes a little more sense now: JavaScript is a single-threaded, asynchronous language with a concurrency model that is based on a “run to completion” event loop.

Thanks for reading, as always feel free to connect with me on LinkedIn!

More content at plainenglish.io

--

--