Event Loop - a simple definition
The Event Loop in JavaScript is a fundamental mechanism that allows the language to execute code efficiently while remaining single-threaded. Put simply, it's what enables parts of our code to run asynchronously.
It's the Event Loop that lets JavaScript respond to events, delegate tasks over time, and handle network operations or load files "in the background", ensuring that many operations happen without freezing the application or user interface in web pages.
But how does it work?
To understand how the Event Loop works in JavaScript, we need to be aware of a few key data structures it relies on.
Stack
First, we have the Stack, which stores information in a LIFO (Last-In, First-Out) manner. It works just like a stack of plates: the last plate you place on top is the first one you’ll take off.
Similarly, the information we store in the Stack must follow the same principle - what goes in last comes out first. It's designed in such a way that you can only remove data from the top of the stack, and this is exactly how it's implemented.
Queue
The second important data structure in the Event Loop is the Queue, which works in the opposite way to the Stack. Think of it as a queue at a store: the first customer to join the queue will be the first one to leave - this is the principle of FIFO (First-In, First-Out).
As with the Stack, the Queue is designed to prevent data from being removed in any way other than from the front of it.
How Does This Relate to the Event Loop
It's simple - Event Loop is a mechanism that effectively uses these two types of data structures in practice. Let's break it down step by step:
Example 1: a simplified case
In this example, we'll simply call a declared function and observe the "journey" that it takes until it is executed in the browser.
Step one: JavaScript encounters a function that is called in the source code it is interpreting.
function log(text) {
console.log(`Log: ${text}`);
}
log('Hello!');
Step two: JavaScript moves the called function into the call stack:
Call stack:
> log('Hello!')
Step three: The function, being the only one and the last one added to the stack, is at the top - it is popped off the stack first (LIFO principle) and executed in the browser.
> Log: Hello!
Example 2 - nested functions
Functions that are called inside other functions are pushed onto the stack in reverse order of their appearance.
function first() {
second();
}
function second() {
third();
}
function third() {
return 'Hello!';
}
first();
For this example, the call stack will look like this:
third()
second()
first()
Following the LIFO principle, the functions are popped off the stack and executed one by one, starting with third()
, then second()
, then first()
.
What about the queue?
For asynchronous operations, such as setTimeout
, or event handling, an additional element comes into play: the callback queue. This queue, as a data structure, was described in the section above.
Asynchronous operations don't go directly onto the call stack - they are first placed in the queue, where they wait for execution.
console.log('Log first');
setTimeout(() => console.log('Log last'), 0);
console.log('Log second');
Execution flow:
console.log('Log first');
is a synchronous operation and goes directly onto the call stack.The
setTimeout
function is called synchronously, but its callback is moved to the callback queue.console.log('Log second');
is executed synchronously on the call stack.The browser engine, or any other JavaScript execution environment like Node.js, takes the control over the
setTimeout
and when the right time comes (in this case: zero miliseconds after the rest of the code is executed), gives its callback function to the callback queue. The Event Loop later checks this queue.Once the stack is empty, the Event Loop checks the callback queue and moves the
setTimeout
callback onto the call stack, where it is executed. It's not zero milliseconds after, though - the call stack might be emptied few milliseconds later, so the exact time that the asynchronous parts can be executed is never definite.
Mikrotasks and their own queue?
Asynchronous operations triggered by callbacks are placed in the "standard" callback queue. However, in the JavaScript world, we also have other important mechanisms for invoking asynchronous operations - for example, Promises. Asynchronous code associated with them has its own queue, called the "microtask queue".
It has priority over the standard callback queue, which means that callbacks always execute only when the microtask queue is emptied - in short, synchronous code always has priority, then asynchronous code invoked using Promises, and only then the standard callbacks.
Final thoughts
The Event Loop enables JavaScript to handle asynchronous operations efficiently in a single-threaded environment. By combining the call stack and the callback queue, it manages the execution of functions and background operations, ensuring the smooth operation of applications.
I hope this post managed to explain the mechanism in a simple way - first by detailing the data structures it uses, and then by illustrating how it works with examples. The Event Loop is one of the most important aspects of programming in JavaScript. Understanding it not only helps you deal with asynchronous code but also avoids common issues like blocking or race conditions.