What happens when the following code is run in Node.js?
setTimeout(() => console.log(1), 10)
Promise.resolve().then(() => console.log(2))
console.log(3)
If your answer was different from:
3
2
1
Perhaps you don't fully understand the execution order of JavaScript and the operation of the Event Loop.
No worries, I'll try to explain.
First of all, if you have doubts about what is:
- JavaScript
- ECMAScript
- JavaScript Runtime
I recommend that you read the glossary before continuing.
Now, let's go, I will explain what happens at each stage of the execution of this JavaScript code.
Main Thread
Node interprets the JavaScript file from top to bottom, line by line, in a single thread.
Running setTimeout()
The main thread will interpret the first instruction, add it to the Call Stack, where it will be executed and removed from the Call Stack.
The setTimeout
instruction is used to schedule the execution of a function after certain milliseconds.
This function is part of the libuv
library, which Node uses to create a Timer without blocking the main thread.
After starting the Timer, the main thread will remove the instruction from the Call Stack.
At the end of the interval, the timer will add the callback of the setTimeout function to the macro-task queue.
Running Promise.resolve().then()
While the Timer of the libuv library waits for the 10ms, the Main thread will interpret the next line of the file.
The instruction this time is
Promise.resolve().then(() => console.log(2))
The main thread will execute the function Promise.resolve().then()
Promise is an object that represents a completion or failure of an asynchronous operation.
By calling the resolve() function without any parameter, we are declaring Promise that does not return any value, but that's okay.
For now, we are more interested in the behavior of the .then function of a Promise.
By passing () => console.log(2)
as callback for our Promise, we are telling Node to execute this code as soon as the Promise is successfully finished.
In other words, we are saying that, as soon as the resolve() method of the Promise is executed, Node should execute our console.log(2) instruction.
But, that's not exactly how it works.
Every Promise callback is sent instantly to a special queue called Micro Tasks Queue.
Recapping
This is the current state of the script execution:
Everything that happened so far, surely, took less than 10 milliseconds, which is why the Timer has not yet added the instruction of console.log(1)
to the Macro Tasks Queue.
But, by using libuv, the Main thread can continue working normally, in a non-blocking manner.
Ok, you might be wondering:
Event Loop
Throughout this process, with each interpretation of a new line from the file, the Event Loop performed a very important, albeit repetitive function.
- Check if the Call Stack was empty.
As you can see, the answer was always: NO!
At no time during the execution of this script was the Call Stack empty, so our friend Event Loop will keep waiting.
Emptying the Call Stack
Now, the Main Thread interprets the last instruction of the file.
This is a simple instruction, which displays a value on the console, its result is:
3
And, for the first time, the Call Stack is empty!
Event Loop
Now, the most awaited moment for the Event Loop, the moment when it has the power to act!
It will only validate the other queues when the Call Stack is empty!
At each loop, it will:
- Process all tasks in the Micro Tasks queue
- Adding them to the Call Stack
- Process 1 task from the Macro Tasks queue
- Adding it to the Call Stack
- Wait for the Call Stack to empty
- Repeat
The Main Thread executes every instruction in the main context.
Now, continuing the execution of the example code:
Micro Tasks
When the Call Stack becomes empty, it means that the Main Thread is not executing anything.
Then, the Event Loop consumes all tasks from the Micro Tasks Queue and adds them to the Call Stack.
Next, the Main Thread consumes the instruction from the Call Stack and executes it.
console.log(2) // Writes 2 to the console
Now, the Call Stack becomes empty again.
Then, the Event Loop looks for more tasks in the Micro Tasks queue.
As it is empty, it finishes its work in the Micro Tasks Queue and starts consuming the Macro Tasks Queue.
Macro Tasks
Now, suppose that the 10-millisecond interval has passed and the Timer has inserted the console.log(1) function into the Macro Tasks queue, the Event Loop will transfer 1 instruction from the Macro Tasks Queue to the Call Stack.
Then, the Main Thread consumes the last instruction from the Call Stack and executes it.
console.log(1) // Writes 1 to the console
Important point: If there were still instructions in the Micro Tasks queue, these would be processed. But, as everything is empty, the program execution is heading towards the end.
That's why the code:
setTimeout(() => console.log(1), 10)
Promise.resolve().then(() => console.log(2))
console.log(3)
Will result in:
3
2
1
We've reached the end - Arlindo Cruz
Now you understand what happens behind the scenes of JavaScript. The Event Loop manages the queues of micro and macro tasks and, with that, ensures that asynchronous instructions are executed harmoniously in the context of the main thread.
Understanding how it works helps us write more efficient codes and better predict the behavior of our applications.
Next time you're writing JavaScript code, I hope you remember everything that happens behind the scenes of the Event Loop.
See you later!
Glossary
JavaScript
It's a high-level, dynamic, interpreted programming language that supports multiple programming paradigms (functional, imperative, object-oriented).
It's a "medium" of conversation between something you want to do and what the computer executes.
ECMAScript
It's a set of rules that defines how JavaScript should work, it defines the language standards (syntax, data types, control structures, and operators), and JavaScript is the implementation of these standards.
If you want to understand better, read this article
JavaScript Runtime
It's the engine that executes JavaScript code.
When writing JavaScript code, you write instructions (which follow the rules defined by ECMAScript), but to execute these instructions, you need a Runtime.
It's as if JavaScript were a recipe and the Runtime was a cook who executes the recipe.
Node, V8 and SpiderMonkey are the most well-known JavaScript runtimes in the world.
Top comments (1)
Muito boa a explicação
Event Loop é um assunto bem difícil de fixar, pois a gente não mexe diretamente com ele não dia a dia (mas sempre precisa dele), então me parece algo mais teórico.
Sempre que encontro um artigo sobre ele eu leio, e vou fixando aos poucos
Parabéns