If Javascript Is Single Threaded, How Is It Asynchronous?

I discovered that JavaScript is a single-threaded and single-concurrent language, which seemed peculiar when compared to other languages that I was familiar with. I found myself pondering over how asynchronous tasks are executed if it is single threaded.

This curiosity sparked a journey of exploration, encompassing extensive reading, research, and hands-on experimentation within the browser.

If you're new to JavaScript, this article will provide valuable insights into why JavaScript behaves differently and why callbacks are essential. And if you're an experienced JavaScript developer, it will offer fresh perspectives on how the underlying Runtime functions, enabling you to enhance your understanding.

To comprehend the JavaScript Runtime, let's take a simplified view of V8, the Runtime used in Chrome. It consists of two primary components:

  • Heap - responsible for memory allocation, and the
  • Call stack - where stack frames reside.

Simplified view of V8

Simplified view of V8


However, if you were to explore the V8 code base and search for terms like "setTimeout," "DOM," or "HTTP request," you would be surprised to find that they are not present within V8 itself. These functionalities exist outside the V8 Runtime.

Now, let's zoom out and examine the bigger picture. Alongside the V8 Runtime, we have additional components known as web APIs, which are provided by the browser. These web APIs include the Document Object Model (DOM), AJAX, timeouts, and more. Furthermore, there is the mythical event loop and the callback queue.

Basic JS runtime architecture

Basic JS runtime architecture


My goal is to help you understand how these pieces fit together. While some of the following information may be familiar to you, it might be new to others. However, we'll quickly progress, so please bear with me if certain concepts seem obvious; they may not be for everyone.

Call Stack

Let's start with the basic concept of the call stack. The call stack is a data structure that keeps track of the execution context of functions in the program. Whenever a function is called, a corresponding stack frame is added to the call stack. When a function finishes executing, its stack frame is removed from the call stack.

When you run a JavaScript file, the main function is initially pushed onto the call stack. As functions are called and returned, the stack frames are added and removed accordingly.

Let's see an example to gain a better understanding.

Consider the code snippet on the left side of your screen. We have a few functions: multiply which multiplies two numbers, square which calls multiply with the same number twice, and a function that prints the square of a number by invoking square and then console.log. Finally, at the bottom of our file, we execute the printSquare function.

Call Stack Example

Call Stack Example


When you run this code, there is a main function, which we put on a stack. Then we have some function definitions. Finally, we reach the printSquare function, so we put it on the stack. Inside printSquare, there is a square function, so we add it to the stack, which calls the multiply function. We add multiply to the stack. Now, we have a return statement. We multiply a and b and return the result. When we return, we remove it from the stack. We return to square, then return to printSquare. In the end, we reach the console.log statement. We reach the end of our function and return.

Does this make sense to you? Great. Let's proceed.

Event Loop

The next piece is the event loop that allows for asynchronous and non-blocking behaviour. In JavaScript, the runtime is single-threaded, meaning it can only execute one piece of code at a time. The event loop enables JavaScript to handle asynchronous operations efficiently without blocking the execution.

Now, let's consider the issue of blocking behaviour. Blocking refers to code that is slow and prevents the program from moving forward until it completes. For example, a synchronous network request or a long-running loop can block the execution of other code.

In a single-threaded programming language like JavaScript, blocking operations can cause the entire program to halt until they are completed. This is particularly problematic in a browser environment, where a blocked program can result in an unresponsive UI.

Event Loop Visualization

Event Loop Visualization


To address this issue, JavaScript introduces asynchronous callbacks as a way to handle non-blocking operations. Instead of waiting for a slow operation to complete, JavaScript can delegate the task to another component, such as a web API provided by the browser. These web APIs, like the DOM, AJAX, or timers (e.g., setTimeout), run concurrently outside of the JavaScript runtime.

When an asynchronous operation is initiated, such as a setTimeout callback, it is handed off to the browser's web API. The web API manages the execution of the operation and, upon completion, places the corresponding callback function into a task queue (also known as the callback queue).

Here's where the event loop comes into play. The event loop continuously monitors both the call stack and the task queue. If the call stack is empty, it takes the first callback from the task queue and pushes it onto the call stack for execution. This process ensures that callbacks are only executed when the stack is clear, allowing the program to handle asynchronous operations efficiently without blocking the execution.

By leveraging the event loop and asynchronous callbacks, JavaScript can achieve concurrency and handle non-blocking operations effectively. It allows the program to continue executing other tasks while waiting for slow operations to complete, resulting in a more responsive and efficient application.

I've developed a guide called Master JavaScript Promises. It's a beginner-friendly guide with one goal: to help you build your intuition of how Promises works, so that you can work with async functions confidently.

I have created a set of resources for learning asynchronous JavaScript. These guides—Callbacks, Promises, and Async/Await —cover everything I’ve learned from years of real-world JavaScript experience.

If you found this article helpful, you’ll get so much out of these guides. Each one is optimized for those “lightbulb moments,” building a strong mental model for how asynchronous JavaScript works and how you can use it to create fast, dynamic applications.