sajad torkamani

The problem: JavaScript is single-threaded

JavaScript is single-threaded so it only works with a single call stack and can only execute one instruction at a time. If the main thread is blocked by a slow operation such as a network request, no other functions in the call stack can be processed.

The browser has to wait for the function at the top of the call stack to return before it can process the rest of the stack (user clicks, scrolls, etc.). The result is an unresponsive web page.

The solution: Offload asynchronous tasks to Web API threads

To circumvent the single-threaded nature of JavaScript, web browsers provide Web APIs like setTimeout and fetch that offload asynchronous or slow operations into background threads that can execute without blocking the main thread.

A typical JavaScript environment

Let’s understand how a web browser does this by considering how it processes the following code one step at a time:

const sayHello = () => console.log('Hello')

const fetchData = () => {
  fetch('https://jsonplaceholder.typicode.com/todos/1')
    .then(function fetchDataCallback(response) {
      console.log(response.json())
    })
}

const sayBye = () => console.log('Bye')

sayHello()
fetchData()
sayBye()

Step 1

sayHello is invoked and pushed to the call stack. The JavaScript runtime (e.g., Chrome’s V8) is responsible for interpreting source code and managing the call stack.

Call stack

sayHello()

Callback queue

<empty>

Step 2

sayHello calls console.log('Hello') which is pushed to the call stack.

Call stack

console.log('Hello')
sayHello()

Callback queue

<empty>

Step 3

console.log('Hello') returns immediately and is popped off the call stack.

Call stack

sayHello()

Callback queue

<empty>

Step 4

sayHello() returns undefined and is popped off the call stack.

Call stack

<empty>

Callback queue

<empty>

Step 5

fetchData() is invoked and pushed to the call stack.

Call stack

fetchData()

Callback queue

<empty>

Step 6

fetchData() calls fetch() which is pushed to the call stack.

Call stack

fetch()
fetchData()

Callback queue

<empty>

Step 7

The fetch Web API stores the fetchDataCallback callback passed to it and initiates a network request in a new background thread (not the main thread). The fetch method itself returns immediately and is popped off the main thread’s call stack (the network request runs in a background thread).

Call stack

fetchData()

Callback queue

<empty>

Step 8

fetchData() returns undefined and is popped off the stack.

Call stack

<empty>

Callback queue

<empty>

Step 9

sayBye() is invoked and pushed to the call stack.

In the meantime, the browser finishes the network request that it performed in a separate thread and adds the fetchDataCallback callback (provided earlier) to the callback queue.

Call stack

sayBye()

Callback queue

fetchDataCallback

Step 10

The event loop, always running in the background, sees that the callback queue is not empty, but doesn’t process it yet because the call stack still has items. It only processes the callback queue when the call stack is empty.

Thus, the first item in the stack – sayBye()– is called which invokes console.log('Bye'). console.log('Bye') is pushed to the call stack.

Call stack

console.log('Bye')
sayBye()

Callback queue

fetchDataCallback

Step 11

console.log('Bye') returns undefined and is popped off the call stack.

Call stack

sayBye()

Callback queue

fetchDataCallback

Step 12

sayBye() returns undefined and is popped off the call stack (The event loop is still waiting for the call stack to be empty before it processes the callback queue).

Call stack

<empty>

Callback queue

fetchDataCallback

Step 13

The event loop sees that the call stack is empty and that there’s an item in the callback queue. So, it pushes the first item in the callback queue onto the call stack.

Call stack

fetchDataCallback

Callback queue

<empty>

Step 14

fetchDataCallback is invoked and popped off the call stack.

Call stack

<empty>

Callback queue

<empty>

Recap of steps

As you can see, asynchronous operations like network requests or timers can be offloaded by the Web APIs to background threads. Most asynchronous operations take a callback as an argument so that when they complete, the callback can be added (perhaps with the response of the operation) to the callback queue.

The event loop is always running in the background and when it sees an empty call stack and an non-empty callback queue, it adds the first time in the queue to the call stack. In this way then, web browsers simulate concurrency.

Tagged: JavaScript