If you’ve been working on significant JavaScript applications for a while, you’ve likely heard engineers using terms like “non-blocking”, “single threaded” and “the event loop”. If you’re anything like me, for a long time I had only a superficial understanding of what those terms meant and even less of an idea how JavaScript worked under the hood to account for them.
In this post I’m going to attempt to provide an overview of the event loop and why it is important in the JavaScript world. By the end of it, you should have a better understanding of how JavaScript executes your code and as a result a better insight into how concurrency is handled in JavaScript.
(I should mention that the content of this article draws heavily on this excellent talk by Philip Roberts – I strongly recommend you watch the video after reading this article).
The Callstack
The first thing we need to discuss is the callstack (a.k.a “stack”). The JavaScript runtime is single threaded. This means it can only run 1 piece of code at a time. The execution order of this code is handled by the callstack.
Just like any stack, things get “pushed” onto and “popped” off it at given times.
The stack records wherein the program execution we currently are. When a function is called it gets pushed onto the stack. When a function returns it pops off the stack.
The stack may already be familiar to you – it’s what you see in the debugger tools when you encounter an error (the “stack trace”).
Blocking and the Callstack
As the JS runtime is single threaded the stack is executed in order. As a result, if some code on the stack is slow to return then the program execution is stalled until it is finished. This is what we mean when we refer to “blocking” the stack.
An example of a piece of code that might block the stack would be a synchronous Ajax request (don’t ever do this!).
If the stack becomes blocked the browser cannot do anything – that means no ability to run other code or even update the browser UI. In such a scenario the browser is said to “freeze” as it appears totally unresponsive until the stack becomes unblocked. Obviously this is terrible for UX.
To work around the synchronous nature of the callstack, JavaScript makes heavy use of callbacks (or deferred values – eg: Promises).
Concurrency via Environment APIs
As we explained above the JS runtime itself can only do 1 thing at a time. However, the JavaScript environment is more than just the JS runtime.
If we take the browser for example, then we discover that it provides a set of webapis which are part of the internals of the environment. As developers, we cannot interact with the internals of these apis (they are usually marked as [native code] in the debugger) but they can be thought of as extra threads that we can access via the interfaces that they provide to us.
It is these apis that provide concurrency in JavaScript. NodeJS provides similar apis, but instead of webapis these are C++ apis. Examples of such apis include:
- setTimeout
- XMLHttpRequest
- DOM
Handling concurrency
The process by which the environment handles concurrency is approximately as follows:
- your code is pushed onto Stack and executed
- once it completes it pops off the Stack
- if your code called a webapi (eg: setTimeout) then this is handled behind the scenes by the environment – it does not interact with the JS runtime
- when the environment is finished handling the webapi, then any callbacks you passed to it are pushed onto the task queue
At this point it is important to note the distinction between the callstack and the task queue. The former handles the execution of your code (as described above) whereas the task queue is a place for webapis to queue up callbacks for execution by the Event loop.
The Event Loop
The Event loop is responsible for managing the interaction between the task queue and the callstack. It happens as follows:
- The Event loop waits until the stack is empty
- It then checks to see if there is anything on the task queue
- If there is then it pops the first item off the task queue and pushes it onto the callstack
- the callback is then executed
The key takeaway here is that the event loop is responsible for ensuring that items on the task queue are pushed onto the stack only when the stack is empty.
Real life Event loop – Deferring code until the Stack is clear
If you’ve been doing JS for sometime you may have come across something like this:
setTimeout(() => {
// Defer until next tick
doSomething();
}, 0);
But what the heck does that mean? Run the doSomething function after 0 seconds perhaps? Nope!
If you’ve been paying attention, then you will recall that the Event loop must wait for the callstack to be empty before it can pop from the task queue onto the stack.
As a result, the code above will defer execution of the callback until after the current callstack is empty (or more precisely, on the next “tick” of the event loop). This is why if we passed 1000 as the 2nd param to the setTimeout it wouldn’t guarantee that the callback would be executed after 1 second. Rather it guarantees the callback will be added to the event loop after 1 second, at which time it will get executed on the next tick of the event loop.
Summary
So there you have it. A whistle-stop tour of the Event loop in JavaScript. For a much more thorough overview of this topic, I highly recommend this excellent talk by Philip Roberts which provides visual diagrams which should help to further illuminate the concepts discussed here.