My goal with this article is to teach you how JavaScript works in the browser. Even though I’ve been working with JavaScript my whole career, I didn’t get how these things work until recently.
I still forget how this works from time to time. That’s why I wrote this article. I hope it will make you understand these concepts as well.
Contents
Before I dive into the explanation of each topic, I want you to have a look at this high-level overview that I created, which is an abstraction of how JavaScript interacts with the browser.
Don’t worry if you don’t know what all of the terms mean. I will cover each of them in this section.
Note how most of the things in the graphic aren’t part of the JavaScript language itself. Web APIs, the callback queue, and the event loop are all features that the browser provides.
A representation of NodeJS would look similar, but in this article, I’ll focus on how JavaScript works in the browser.
You’ve probably already heard that JavaScript is single-threaded. But what does this mean?
JavaScript can do one single thing at a time because it has only one call stack.
The call stack is a mechanism that helps the JavaScript interpreter to keep track of the functions that a script calls.
Every time a script or function calls a function, it’s added to the top of the call stack. Every time the function exits, the interpreter removes it from the call stack.
A function either exits through a return statement or by reaching the end of the scope.
I created this small visualization to make it easier to understand:
const addOne = (value) => value + 1;
const addTwo = (value) => addOne(value + 1);
const addThree = (value) => addTwo(value + 1);
const calculation = () => {
return addThree(1) + addTwo(2);
};
calculation();
addThree(1)
calculation()
main()
Note how each function call is being added to the call stack and removed once it finishes.
Every time a function calls another function, it’s added to the top of the stack, on top of the calling function.
The order in which the stack processes each function call is following the LIFO principle (Last In, First Out).
The steps of the previous example are the following:
main
function is being called, which stands for the execution of the entire file. This function is added to the call stack.main
calls calculation()
, which is why it is added to the top of the call stack.calculation()
calls addThree()
, which again is added to the call stack.addThree
calls addTwo
, which is added to the call stack.…
addOne
doesn’t call any other functions. When it exits, it is removed from the call stack.addOne
, addTwo
exits as well and is being removed from the call stack.addThree
is being removed as well.calculation
calls addTwo
, which adds it to the call stack.addTwo
calls addOne
and adds it to the call stack.addOne
exits and is being removed from the call stack.addTwo
exits and is being removed from the call stack.calculation
can exit now with the result of addThree
and addTwo
and is being removed from the call stack.main
exits as well and is being removed from the call stack.I called the context that executes our code
main
, but this is not how the official name of the function. In the error messages that you can find in the browser’s console, the name of this function isanonymous
.
You probably know the call stack from debugging your code. Uncaught RangeError: Maximum call stack size exceeded
is one of the errors you might encounter. Below we can see a snapshot of the callstack when the error occured.
Follow the stack trace of this error message. It represents the functions calls that led to this error. In this case, the error was in the function b, which has been called by a (which has been called by b and so on).
If you see this specific error message on your screen, one of your function has called too many functions. The maximum call stack size ranges from 10 to 50 thousand calls, so if you exceed that, it’s most likely that you have an infinite loop in your code.
The browser prevents your code from freezing the whole page by limiting the call stack.
I re-created the error with the following code. A way to prevent this is by either not using recursive functions in the first place, or by providing a base case, which makes your function exit at some point.
function a() {
b();
}
function b() {
a();
}
a();
In summary, the call stack keeps track of the function calls in your code. It follows the LIFO principle (Last In, First Out), which means it always processes the call that is on top of the stack first.
JavaScript only has one call stack, which is why it can only do one thing at a time.
The JavaScript heap is where objects are stored when we define functions or variables.
Since it doesn’t affect the call stack and the event loop, it would be out of the scope of this article to explain how JavaScript’s memory allocation works.
Above, I said that JavaScript can only do one thing at a time.
While this is true for the JavaScript language itself, you can still do things concurrently in the browser. As the title already suggests, this is possible through the APIs that browsers provide.
Let’s take a look at how we make an API request, for instance. If we executed the code within the JavaScript interpreter, we wouldn’t be able to do anything else until we get a response from the server.
It would pretty much make web applications unusable.
As a solution to this, web browsers give us APIs that we can call in our JavaScript code. The execution, however, is handled by the platform itself, which is why it won’t block the call stack.
Another advantage of web APIs is that they are written in lower-level code (like C), which allows them to do things that simply aren’t possible in plain JavaScript.
They enable you to make AJAX requests or manipulate the DOM, but also a range of other things, like geo-tracking, accessing local storage, service workers, and more.
With the features of web APIs, we’re now able to do things concurrently outside of the JavaScript interpreter. But what happens if we want our JavaScript code to react to the result of a Web API, like an AJAX request for instance?
That’s where callbacks come into play. Through them, web APIs allow us to run code after the execution of the API call has finished.
A callback is a function that’s passed as an argument to another function. The callback will usually be executed after the code has finished.
You can create callback functions yourself by writing functions that accept a function as an argument. Functions like that are also known as higher-order functions. Note that callbacks aren’t by default asynchronous.
Let’s have a look at an example:
const a = () => console.log('a');
const b = () => setTimeout(() => console.log('b'), 100);
const c = () => console.log('c');
a();
b();
c();
setTimeout
adds a timeout of x ms before the callback will be executed.
You can probably already think of what the output will look like.
setTimeout
is being executed concurrently while the JS interpreter continues to execute the next statements.
When the timeout has passed and the call stack is empty again, the callback function that has been passed to setTimeout
will be executed.
The final output will look like this:
a
c
b
Now, after setTimeout
finishes its execution, it doesn’t immediately call the callback function. But why’s that?
Remember that JavaScript can only do one thing at a time?
The callback we passed as an argument to setTimeout
is written in JavaScript. Thus, the JavaScript interpreter needs to run the code, which means that it needs to use the call stack, which again means that we have to wait until the call stack is empty in order to execute the callback.
You can observe this behavior in the following animation, which is visualizing the execution of the code we saw above.
Calling setTimeout
triggers the execution of the web API, which adds the callback to the callback queue. The event loop then takes the callback from the queue and adds it to the stack as soon as it’s empty.
Multiple things are going on here at the same time. Follow the path that the execution of setTimeout
takes, and in another run, focus on what the call stack does.
Unlike the call stack, the callback queue follows the FIFO order (First In, First Out), meaning that the calls are processed in the same order they’ve been added to the queue.
#javascript #web-development #programming #developer