You may have heard that JavaScript has a thing called the “event loop”. But what is it actually and what does it really mean? Why it’s important to know and what is the benefit of knowing about it? In this tutorial, we will explore the questions and simplify a seemingly complex idea.
In one single sentence, let’s define what is the event loop:
Moving events from the task queue to the call stack is called the “event loop”
So then what is this task queue and call stack? And how do they come into play? Let’s have a look at a simplified version of a browser’s architecture to have a deeper understanding of what happens when your JavaScript code is being executed.
Inside the browser, we have four main components. The one we are interested in is the event loop. But in order to understand the job of the event loop, we need to clear the fog around the other three: the call stack, the web API thread, and the task queue.
The Call Stack
As you may have already know, JavaScript is a single-threaded language, which means it only has one call stack, unlike other multi-threaded languages.
This also means that it can only execute one code at a time. The order in which this code is being executed is handled by the call stack. It holds information about where we are in the code right now, in terms of execution order.
To fully understand its workings, let’s demonstrate it through an example:
console.log('Learning');
console.log('About');
console.log('The Event Loop');
We all know what will happen. We are going to see Learning About The Event Loop
in the console. But what happens inside the call stack? Let’s break it down how it is handled, line by line:
- We start off at line:1; We push
console.log
onto the top of the call stack and pop it off immediately as we return implicitly. - We do it for the other two lines as well. We push
console.log
onto the stack and pop it off once it’s executed.
Let’s look at another example, what do you think will happen?
const recursion = () => {
recursion();
};
recursion();
We define a function called recursion and we call it on line:5. Then inside the function, we call it again and again and again… Which will give us an error:
We keep pushing recursion
onto the stack without ever popping off one item, leading to a RangeError
, which prevents the browser from crashing.
You can also see the stack trace — the order in which your code is being executed — below the error message.
That’s all the call stack does. It pushes code onto the stack and pops them off once they are executed. It’s basically a snapshot of the current state of your program; where you are when the JavaScript engine executes your code.
So if JavaScript is single-threaded and we are only able to run one piece of code at a time, then how come we have asynchronous functionality? — Meaning we are non-blocking. This is where browser or web APIs come into play.
The Web API Thread
Let’s take a look at the following example now:
console.log('🐹');
setTimeout(() => console.log('🐹🐹'), 0);
console.log('🐹🐹🐹');
What do you think the output will be in the code example above? If you guessed:
🐹
🐹🐹🐹
🐹🐹
You’ve either learned it on the hard way or you already know how the event loop works. Maybe both. But why not:
🐹
🐹🐹
🐹🐹🐹
Even though we call setTimeout
with 0 milliseconds, it is still being executed as the last thing. Let’s quickly go through how the code above is being handled by the call stack once more:
- We start at line:1, we push the
console.log
into the call stack and we pop it off immediately, so we have ‘🐹’ in the console. - We arrive at line:3, we push
setTimeout
into the stack, but we can’t execute it becausesetTimeout
is not part of the V8 engine. It’s not a core JavaScript function, it’s a browser API. It is added to JavaScript through the browser. This is the reason you need to polyfill thePromise
object or the DOM inside node as they are provided by the browser. These API calls are handled in a different thread, so we initiate a call to the web API thread and we popsetTimeout
off the stack. This starts a timer in another thread pool, outside of the JavaScript world. - We continue our execution and we push the other
console.log
on line:5 onto the stack and we pop it off, so now we have ‘🐹🐹🐹’
This is the current state of the browser. We have an empty call stack, but we have one item waiting to be executed in the thread which handles web API calls. This holds our last console.log
.
Now whenever the call to setTimeout
is finished (which is instantaneous in this case since we passed in 0 milliseconds), we want to execute it. In order to do so, we need to move it back onto the stack. Since this can finish anytime in the future but we don’t know when, we can’t just push it onto the stack as it would mean our code is not deterministic. It would appear randomly at some point in time.
Think of a network request. It can finish in 300ms or never or anything in between. So instead, it is pushed into the task queue.
The Task Queue
The task queue — often called the callback queue or the event queue — is responsible for collecting the return value of each successfully executed web API call.
Whenever our setTimeout
finishes, its callback is moved into the task queue. In our case, the last console.log
.
So now we have an empty call stack and an empty web API thread. But we have the console.log
waiting in the task queue. If we were to have multiple web API calls, we would push them into the task queue one after the other.
So how do we get events from the task queue into the call stack? This is where the event loop comes in, the protagonist of the article.
The Event Loop
From all of the above, the event loop is probably the simplest piece among all: Its job is to look at the task queue and the call stack;
If the call stack is empty and we have events waiting to be executed in the task queue, it pushes the first event from the task queue back into the call stack. And it keeps going until the task queue is empty. That’s all the event loop does:
// If the call stack is empty and the task queue is not
// move the first event from the task queue into the call stack
if (callStack.isEmpty && taskQueue.length) {
eventLoop();
}
So now our last console.log
is being pushed back into the call stack, which is executed again and hence we get:
🐹
🐹🐹🐹
🐹🐹
With a 0 milliseconds setTimeout
, we essentially told JavaScript to defer the call until the stack is empty.
So the next time you’re on an interview and the interviewer asks you questions like: What is the event loop? or How can it be that JavaScript is asynchronous and single-threaded at the same time? — hopefully, you will know the answer.
Recommendation
If you’re still in doubt however and this tutorial didn’t make any sense to you, then I’m probably really bad explaining things and sharing knowledge. In any case, if you want to dive deeper into the things mentioned above and you’re more of the visual type, I can highly recommend What the heck is the event loop anyway by Philip Roberts.
He helped me understand the event loop once and for all. This article may heavily reflect his teachings as he explains it in a clear and understandable way. He goes into great detail explaining the inner working of the JavaScript Runtime.
Do you have some additions that should be mentioned? Let us know in the comments below! Thank you for reading through, happy coding!
Top comments (1)
Thanks