Today, in the sixth part of the translation guide for Node.js, we will talk about the event loop, the call stack, the
process.nextTick()
function, and timers. Understanding these and other Node.js mechanisms is one of the foundations for successful application development for this platform.

[We advise to read] Other parts of the cyclePart 1:
General Information and Getting StartedPart 2:
JavaScript, V8, some design tricksPart 3:
Hosting, REPL, work with the console, modulesPart 4:
npm, package.json and package-lock.json filesPart 5:
npm and npxPart 6:
event loop, call stack, timersPart 7:
Asynchronous ProgrammingPart 8:
Node.js Guide, part 8: HTTP and WebSocket protocolsPart 9:
Node.js Guide, part 9: working with the file systemPart 10:
Node.js Guide, part 10: standard modules, streams, databases, NODE_ENVFull PDF Node.js Manual Event cycle
If you want to understand how the JavaScript code is executed, then the Event Loop is one of the most important concepts that you need to understand. Here we will talk about how JavaScript works in a single-threaded mode, and how the asynchronous functions are handled.
')
For many years I have been developing in JavaScript, but I cannot say that I fully understood how everything functions, so to speak, “under the hood”. The programmer may not know about the intricacies of the internal subsystems of the environment in which he works. But it is usually useful to have at least a general idea of ​​such things.
The JavaScript code you write is executed in single-threaded mode. At a time, only one action is performed. This restriction is, in fact, quite useful. This greatly simplifies the way programs work, eliminating the need for programmers to solve problems specific to multi-threaded environments.
In fact, the JS programmer needs to pay attention only to what actions his code performs, and at the same time try to avoid situations that cause blocking of the main thread. For example, making network calls in synchronous mode and endless
loops .
Usually in browsers, each open tab has its own event loop. This allows you to perform the code of each page in an isolated environment and avoid situations where a certain page, in the code of which there is an infinite loop or heavy calculations are performed, can “hang” the entire browser. The browser supports the operation of a set of simultaneously existing event cycles, used, for example, to handle calls to various APIs. In addition, its own event loop is used to support
web workers .
The most important thing that a JavaScript programmer needs to constantly remember is that his code uses its own event loop, so the code must be written so that this event loop does not block.
Locking event loop
Any JavaScript code that takes too much time to execute, that is, code that does not return control to an event loop for too long, blocks the execution of any other page code. This even leads to the blocking of user interface event handling, which is expressed in the fact that the user cannot interact with the page elements and work with it normally, for example, scrolling.
Virtually all the basic mechanisms for providing I / O to JavaScript are non-blocking. This applies to both the browser and Node.js. Among such mechanisms, for example, are the tools for performing network requests, used in both client and server environments, and tools for working with Node.js files. There are synchronous ways to perform such operations, but they are used only in special cases. That is why in JavaScript, traditional callbacks and newer mechanisms — promises and async / await constructions — are of paramount importance.
Call stack
Call Stack in JavaScript is based on the LIFO principle (Last In, First Out - last entered, first out). The event loop constantly checks the call stack to see if it has a function to perform. If, when executing the code, it encounters a call to some function, information about it is added to the call stack and the function is executed.
If even before you were not interested in the concept of “call stack”, then you, if you met with error messages that include the stack trace, already imagine what it looks like. Here, for example, how similar looks in the browser.
Browser error messageThe browser, when an error occurs, reports a sequence of function calls, information about which is stored in the call stack, which allows detecting the source of the error and understanding which function calls led to the current situation.
Now that we have talked in general about the event loop and the call stack, consider an example illustrating the execution of a code snippet, and how this process looks in terms of the event loop and the call stack.
Event loop and call stack
Here is the code with which we will experiment:
const bar = () => console.log('bar') const baz = () => console.log('baz') const foo = () => { console.log('foo') bar() baz() } foo()
If this code is executed, the following will get to the console:
foo bar baz
This result is quite expected. Namely, when this code is run, the function
foo()
first called. Inside this function, we first call the function
bar()
, and then
baz()
. In this case, the call stack during the execution of this code undergoes changes, as shown in the following figure.
Changing the state of the call stack when executing the code being examinedThe event loop, at each iteration, checks if there is anything in the call stack, and if so, executes it until the call stack is empty.
Event loop iterationsQueuing a function
The above example looks quite normal, there is nothing special about it: JavaScript finds the code to be executed and executes it in order. Let's talk about how to postpone the function until the call stack is cleared. In order to do this, use this structure:
setTimeout(() => {}), 0)
It allows you to execute the function passed to the
setTimeout()
function after all the other functions called in the program code are executed.
Consider an example:
const bar = () => console.log('bar') const baz = () => console.log('baz') const foo = () => { console.log('foo') setTimeout(bar, 0) baz() } foo()
What this code displays might seem unexpected:
foo baz bar
When we run this example, the function
foo()
is called first. In it, we call
setTimeout()
, passing this function as the first argument,
bar
. Passing
0
as the second argument, we inform the system that this function should be executed as soon as possible. Then we call the
baz()
function.
This is what the call stack will now look like.
Changing the state of the call stack during the execution of the codeHere is the order in which the functions in our program will now be performed.
Event loop iterationsWhy does everything happen that way?
Event queue
When the
setTimeout()
function is called, the browser or the Node.js platform starts a timer. After the timer fires (in our case it happens immediately, since we set it to 0), the callback function passed to
setTimeout()
is put in the Event Queue.
In the event queue, if we are talking about the browser, there are also user-initiated events — events triggered by mouse clicks on the page elements, events triggered when entering data from the keyboard. Immediately there are DOM
onload
handlers like
onload
, functions that are called when receiving responses to asynchronous requests for loading data. Here they are waiting for their turn to be processed.
The event loop gives priority to what is on the call stack. First, it performs everything that it manages to find in the stack, and after the stack is empty, it proceeds to process what is in the event queue.
We do not need to wait until the function, like
setTimeout()
, completes, since such functions are provided by the browser and they use their own threads. So, for example, by setting the timer for 2 seconds with the
setTimeout()
function, you should not, having stopped the execution of another code, wait for these 2 seconds, since the timer runs outside of your code.
ES6 Job Queue
In ECMAScript 2015 (ES6), the concept of a job queue (Job Queue) was introduced, which is used by promises (they also appeared in ES6). Thanks to the job queue, the result of the asynchronous function can be used as quickly as possible, without having to wait for the call stack to be cleared.
If the promise is allowed before the end of the current function, the corresponding code will be executed immediately after the current function completes.
I found an interesting analogy for what we are talking about. This can be compared to a roller coaster in an amusement park. After you ride the hill and want to do it again, you take a ticket and stand at the tail of the queue. This is how the event queue works. But the job queue looks different. This concept is similar to the discount ticket, which gives you the right to make the next trip immediately after you have completed the previous one.
Consider the following example:
const bar = () => console.log('bar') const baz = () => console.log('baz') const foo = () => { console.log('foo') setTimeout(bar, 0) new Promise((resolve, reject) => resolve('should be right after baz, before bar') ).then(resolve => console.log(resolve)) baz() } foo()
Here is what will be displayed after its execution:
foo baz should be right after baz, before bar bar
What can be seen here demonstrates the serious difference between promises (and the async / await design that is based on them) and traditional asynchronous functions, which are organized by
setTimeout()
or other APIs of the platform used.
process.nextTick ()
The
process.nextTick()
method interacts in a special way with the event loop. Tick ​​(tick) call one full pass of the event cycle. By passing the function to the
process.nextTick()
method, we inform the system that this function must be called after the current iteration of the event loop is completed, before the next one begins. Using this method looks like this:
process.nextTick(() => {
Suppose the event loop is busy executing the code of the current function. When this operation completes, the JavaScript engine will execute all functions passed to
process.nextTick()
during the execution of the previous operation. Using this mechanism, we strive to ensure that a certain function would be executed asynchronously (after the current function), but as soon as possible, without putting it in a queue.
For example, if you use the
setTimeout(() => {}, 0)
construct, the function will be executed at the next iteration of the event loop, that is, much later than when using
process.nextTick()
in the same situation. This method should be used when you need to ensure that some code is executed at the very beginning of the next iteration of the event loop.
setImmediate ()
Another function provided by Node.js for asynchronous code execution is
setImmediate()
. Here's how to use it:
setImmediate(() => {
The callback function passed to
setImmediate()
will be executed on the next iteration of the event loop.
How
setImmediate()
differ from
setTimeout(() => {}, 0)
(that is, from the timer that should work as soon as possible) and from
process.nextTick()
?
The function passed to
process.nextTick()
will run after the current iteration of the event loop is completed. That is, such a function will always be executed before a function that is scheduled to be executed using
setTimeout()
or
setImmediate()
.
The call to the
setTimeout()
function with a 0 ms delay set is very similar to the
setImmediate()
call. The order of execution of the functions transferred to them depends on various factors, but in both cases callbacks will be called up at the next iteration of the event loop.
Timers
Above, we have already talked about the
setTimeout()
function, which allows you to schedule calls to the callbacks passed to it. Let us spend some time describing its features in more detail and consider another function,
setInterval()
, which is similar to it. In Node.js, the functions for working with timers are included in the
timer module, but they can be used without connecting this module in the code, since they are global.
SetsetTimeout () function
Recall that when you call the
setTimeout()
function, it is passed a callback and time, in milliseconds, after which a callback will be called. Consider an example:
setTimeout(() => { // 2 }, 2000) setTimeout(() => { // 50 }, 50)
Here we pass
setTimeout()
new function, which is immediately described, but here you can use the existing function by passing
setTimeout()
its name and a set of parameters to start it. It looks like this:
const myFunction = (firstParam, secondParam) => {
The
setTimeout()
function returns a timer identifier. Usually it is not used, but it can be saved, and, if necessary, delete the timer, if the scheduled execution of the callback is no longer necessary:
const id = setTimeout(() => {
â–Ť Zero delay
In the previous sections, we used
setTimeout()
, passing to it as a time after which we had to call a callback,
0
. This meant that the callback would be called as soon as possible, but after the completion of the current function:
setTimeout(() => { console.log('after ') }, 0) console.log(' before ')
This code will output the following:
before after
This technique is especially useful in situations where, when performing heavy computational tasks, I would not want to block the main thread, allowing other functions to be performed, breaking up similar tasks into several stages, arranged as
setTimeout()
calls.
If we recall the aforementioned
setImmediate()
function, then in Node.js it is standard, which is not the case with browsers (it is
implemented in IE and Edge, not in others).
SetsetInterval () function
The
setInterval()
function is similar to
setTimeout()
, but there are differences between them. Instead of once executing the callback passed to it,
setInterval()
will periodically, at a specified interval, call this callback. Ideally, this will continue until the programmer explicitly stops this process. Here is how to use this feature:
setInterval(() => {
The callback passed to the function shown above will be called every 2 seconds. In order to provide the possibility of stopping this process, you need to get the timer identifier returned by
setInterval()
and use the
clearInterval()
command:
const id = setInterval(() => { // 2 }, 2000) clearInterval(id)
A common technique is to call
clearInterval()
inside a callback passed to
setInterval()
when certain conditions are met. For example, the following code will be run periodically until the
App.somethingIWait
property takes the value of
arrived
:
const interval = setInterval(function() { if (App.somethingIWait === 'arrived') { clearInterval(interval) // - , - } }, 100)
â–ŤRecursive setting setTimeout ()
The
setInterval()
function will call the callback passed to it every
n
milliseconds, not caring if the callback has completed after its previous call.
If each call to this callback always requires the same time, less than
n
, then there are no problems here.
Periodically called callback, each session of which takes the same time, within the interval between callsIt is possible that each time it takes a callback, it takes a different time, which is still less than
n
. If, for example, we are talking about performing some network operations, then this situation is quite expected.
Periodically called callback, each session of which takes a different time, within the interval between callsWhen using
setInterval()
, a situation may arise when the execution of a callback takes time longer than
n
, which leads to the fact that the next call is made before the completion of the previous one.
Periodically called callback, each session of which takes a different time, which sometimes does not fit into the interval between callsIn order to avoid this situation, you can use the technique of recursive timer setting using
setTimeout()
. The point is that the next callback call is scheduled after the completion of its previous call:
const myFunction = () => {
With this approach, you can implement the following scenario:
A recursive setTimeout () call to schedule callback execution.Results
Today we talked about the internal mechanisms of Node.js, such as the cycle of events, the call stack, and discussed working with timers that allow you to schedule code execution. Next time we delve into the topic of asynchronous programming.
Dear readers! Have you encountered situations when you had to use process.nextTick ()?