How web browsers process asynchronous code
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.
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
|
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.
Thanks for your comment 🙏. Once it's approved, it will appear here.
Leave a comment