The event loop is a core concept in Javascript. It is fundamental in order to understand how asynchronous code works – and how not to work with synchronous code.
Let’s start with a simple example. You have some code that is dependent on some variable to exist in memory. As long as it is not there, you’d like the code to retry again.
function retry() { | |
try { | |
console.log(window.toBe.orNotToBe); | |
} catch(e) { | |
retry(); | |
} | |
} | |
retry(); | |
// simulate a fetch action from the server | |
setTimeout(() => { | |
window.toBe = {orNotToBe: "That is the question!"} | |
}, 1000); |
The result of this code is the following error:
One can simply see this error, find out that the solution would be to use some asynchronous mechanism and come out happy. If you just want the solution, skip the next part.
If you’d like to learn how this error happens, what is the call stack and what are synchronous and asynchronous events in the browser – read on.
Table of Contents
Stackoverflow?
The famous website – stackoverflow – has a funny logo:
Actually – the term stack overflow
is exactly what happened to our little code. Our function called itself multiple time and filled the call stack – hence we exceeded the maximum call stack size. We overflowed it with callback calls.
What is this call stack?
The call stack is, well… a stack. A stack is a data structure. It’s a kind of array that allows us two basic operations: add to the stack and pop the last added value.
When we call a function it is added to the call stack. When this function calls a function, it is also added to the call stack (let’s call that called function a child).
When a function finishes running, it is removed from the stack. Remember that in a stack, we remove only the last added value. If the function has children, it needs to wait until all of them finish.
Here’s an illustration:
So what’s stackoverflow?
Let’s take what we learned about the call stack. If our function calls itself over and over again, the call stack will never clear. It will just add more and more calls to the same function:
So stack overflow would be the case of a function that calls itself indefinitely:
How does it look in the browser?
Going back to our retry function, and knowing what we know now about the callstack, we can now understand what’s the Maximum call stack size exceeded
error mean.
The call stack can be seen in the browser and not just in fancy gif illustrtaions.
I run the above code in an HTML page and record using the performance tab. Here are the results:
In Figure 5 the retry function is called 688 times (the pink lines). After 688 times, we ran out of call stack space and the app crashed.
Not only our code doesn’t do what we want – it crashes. In order to be able to do what we want – retry periodically until something external happens – we’ll need to make our function asynchronous.
The Solution
Making our function asynchronous is quite easy. Using setTimeout
can do just that:
function retry() { | |
try { | |
console.log(window.toBe.orNotToBe); | |
} catch(e) { | |
setTimeout(retry, 250); | |
} | |
} | |
retry(); | |
// simulate a fetch action from the server | |
setTimeout(() => { | |
window.toBe = {orNotToBe: "That is the question!"} | |
}, 1000); |
When running this code, after around 1 second, we get That is the question!
logged in the console.
Let’s look at the recording result of this code:
The results in Figure 6 show that it took around 1 second to run the final call – which eventually logged the wanted result. In between, we had asynchronous calls to the function in roughly 250ms delay between each call.
We now have our retry mechanism. Problem solved!
Which came first, the chicken or the egg?
We learned about the Call Stack. With that knowledge, we know how synchronous processes work in Javascript.
What about asynchronous processes, like the one created by setTimeout
? Or promises and ajax calls?
Here we go out from the zone of the Call Stack to an area called the Event Loop.
When we set our timeout, what we actually did was ask the system to take the callback given and add it to a callback queue once the timer runs out.
Because our click
function finishes, it is removed and the event loop now has an empty stack to send logger
to once it is set in the callback queue.
If we had more asynchronous events set in between they would be in the callback queue as well – and our logger
might just had to wait a bit longer until they finish and the call stack would be ready for it.
This is, in a nutshell, how asynchronous process come to be.
Not all asynchronous processes were born equal
There are types of asynchronous calls. For instance – there’s the setTimeout call, promises. I/O (e.g. user interaction) etc. Each has a different priority to enter the Event Loop.
For instance, if we have a callback from a setTimeout
and a callback from a resolve Promise
– the Promise
‘s callback wins and gets to the call stack first.
I won’t go into more details than that in this article.
Summary
Let’s recap our journey so far:
- We wanted to create a retry mechanism to some variable on the global scope (window).
- We saw that naively trying to call a retry function will results in stackoverflow. Hence, our solution was to make the consecutive calls to
retry
asynchronous. - We did that using
setTimeout
and it solved our issue. - We then explained how asynchronicity is achieved in Javascript – via the event loop and callback queue.
I hope the mechanism is a bit clearer now. As usual, leave comments below or message me directly if you’d like to debate over this (or anything else 🙂 ).
Thanks a lot to Yonatan Doron from Hodash.dev for a thorough review!