Understanding JavaScript Call Stack, Task Queue and Event Loop

If JavaScript is single threaded, how do async functions work?

Azmain Amin
JavaScript in Plain English

--

Disclaimer: This article is heavily inspired by a conference talk given by Phillip Roberts in EUConf 2014.

One thing critics of JS like to point out is that it is single threaded. When I started coding in JS, this confused me. Then how did JS handle async functions? How can we have things like setTimeout() that seems to work independently from the main thread? Hopefully by the end of the article, you will have answers to all these questions. Another goal of this article is to explain some core advanced concepts of JS: the call stack, the task queue and the event loop, which are fundamental to understanding how JS works within the browser.

Let’s say you wrote some JS code for a web app and shipped it. That JS code is run on the browser. How does the browser know what to do with it? The browser is able to run the JS code because it has a JS engine, which for Chrome is the V8 JavaScript engine. In simple terms, V8 knows how to convert JS code into machine code.

The browser is not just the JavaScript engine. It has its own set of APIs, called the Web APIs. setTimeout() is a part of this set of APIs. When you invoke functions such as setTimeout(), you are using an API function that the browser has exposed to you.

The browser is able to run the JS code because it has a JS engine, which for Chrome is the V8 JavaScript engine.

We have covered two critical pieces of the browser: the JS engine and Web APIs. Now let’s move on to the single threaded nature of JS. When we say JS is single threaded, we mean that JS has one call stack. A call stack is a simple data structure which has two functionalities: it can add(push) or remove(pop) items from the top only. Imagine a bucket where you stack your dishes after you are done washing. You can only add or remove a dish from the top only. When a function in invoked, it’s added to the call stack and when it returns, it’s popped off. That’s how JS keeps track of which functions to run when. A visual example:

Functions being added and removed from a stack.
Screenshot taken from https://www.youtube.com/watch?v=8aGhZQkoFbQ&t=635s .

When we say JS is single threaded, we mean that JS has one call stack. A call stack is a simple data structure which has two functionality: it can add(push) or remove(pop) an item from the top only.

Given JS has only one call stack, the functions at the bottom of the stack are dependent on the functions above them: they won’t be run until all the functions above them has finished executing. If we put an async function that makes a network call on the stack, it is going to freeze the browser until it is complete, as network calls can be comparatively slow. This will make user experience miserable. So how do we tackle this issue? Enter task queue.

A picture speaks a thousand words:

Browser architecture with call stack, web api and task queue.
Screenshot taken from https://www.youtube.com/watch?v=8aGhZQkoFbQ&t=635s .

As setTimeout() is an async call which, in our example, will complete after 5 seconds. When setTimeout() is hit, it is pushed onto the stack and it triggers the timer() function of the web apis. When the timer() finishes, it pushes the callback function to the task queue which is a queue based data structure that holds callback functions.

Then comes the event loop whose job is to see if the task queue has any item in it, and if it does, it waits for the call stack to empty and then pushes the item from the task queue to the call stack. This completes the full cycle for an async function.

Interesting tidbit: If you want a piece of code to run when call stack is empty, wrap it in a setTimeout() with a delay of 0 second. Even though it is 0, it is going to be picked up by the event loop from the task queue and pushed to the call stack when the stack is empty.

Interesting tidbit 2: Job Queues are reserved for Promises. The “then-able” code of Promises are stored in job queues.

Let’s recap:

  • JS is single threaded, meaning it has only one call stack.
  • When functions are invoked, they are pushed onto the call stack. When they return/are complete, they are are popped off.
  • Async functions are not part of the JS engine. They are exposed by Web APIs. When you use one i.e. setTimer(callback, timeInMs), it is pushed onto the call stack, triggers a method i.e. timer(callback, timeInMs) in the Web API and is popped off the call stack, freeing the call stack up.
  • When the function triggered by the async call in the Web API stack is done i.e. timer(callback, timeInMs), the callback function of the async method is pushed on to the callback queue.
  • The Event loop’s job is to move stuff out from the callback queue and back onto the call stack. They do this only when the call stack is empty.

--

--

Full stack software engineer with an entrepreneurial spirit. Machine Learning enthusiast.