What is Node.js?

1 | Characteristics of Node.js

In my opinion, you can feel comfortable with Node.js if you remember and deeply understand these 3 characteristics:

  1. Node.js is a JavaScript Runtime Environment (JRE). Node.js uses "V8" as its JavaScript engine. V8 is the JavaScript engine also used by another JRE: the Google Chrome web browser.
  2. Node.js uses event-driven programming. Therefore, Node.js heavily relies, by default, on non-blocking functions. Node.js is adamant about using non-blocking functions whenever it can.
  3. Node.js uses low level APIs.

Keep cool, I will explain everything. In this article, I will explore the first 2 points by answering these 2 questions:

  1. What is a runtime environment?
  2. What is event-driven programming?

The low-level aspect of Node.js will be explored in other articles: you will have some occasions to see that tasks that look quite common may require dozens of lines of relatively dense and complex code in Node.js. This is what makes back-end programming in JavaScript more difficult and more time-consuming than back-end programming in other languages such as Python, PHP or Ruby for example.

2 | What is a runtime environment?

A runtime environment, also abbreviated to runtime, is software designed to support the execution of computer programs written in some computer language. Schematically, a runtime environment is an intermediate but necessary layer between a higher-level programming language and the lower-level programming language of the Operating System (OS).

In other words, a runtime environment is what makes the execution of a program possible within an OS.

There are 2 main runtime environments for JavaScript:

  1. Web browsers, i.e. software to access websites made dynamic thanks to JavaScript.
  2. Node.js, i.e. software to run JavaScript directly on any major OS (Windows, macOs, Linux).

A runtime environment provides functions, objects and libraries to the higher-level programming language it "runs". In particular, JRE provide 3 fundamental features to JavaScript files:

  1. APIs. These APIs give the JRE its full functionality. For example, the browser provides "web APIs" that let JavaScript programs (or scripts) modify the DOM built by the browser. Similarly, Node.js provides modules that enable JavaScript programs to access the file system of a computer.
  2. A JavaScript engine, or JavaScript interpreter. The JavaScript engine translates JavaScript programs into runnable machine code instructions so it can be executed by the Central Processing Unit (CPU) of the host machine.
  3. An event loop and queues. The event loop manages the execution of JavaScript non-blocking functions that are listed in the queues. It is important to understand that when a JavaScript file is executed by the runtime environment, the JavaScript engine parses the code of the file once. Then, the event loop is able to execute functions previously parsed by the JavaScript engine thanks to the queues.

3 | What is event-driven programming?

3.1 | Contextualization

JavaScript was created in 1995 by Brendan Eich to add interactivity (behavior) to web pages, in the browser (the Netscape Navigator web browser at the time). Interactivity is all about events and event handlers. Do not worry. We will explore these concepts in more depth in the next sections. For now, let us gain an intuitive sense of what these terms refer to in a JavaScript context.

If you click on a button on a web page, an animation can appear on the screen or music can start playing. Events and event handlers are a formal transposition of "if this happens and whenever it happens, then the program does that". In other words, "if an event occurs, the program can handle it". Hence the terms event and event handler. In the browser, users make events happen, the browser can signal these events and, then, can perform related tasks. JavaScript relies entirely on events and event handlers to make a web page as interactive as possible. That is the reason why event-based functions are so important and omnipresent in JavaScript, from the very first version of the language.

Apart from allowing interactivity in the browser, the real power of events and event handlers is that they can manage very efficiently interdependent tasks that can take an undefined amount of time to complete. Operations depending directly on the OS, such as reading the content of a file on a disk, can also make use of events and event handlers. For example, once the content of the file on the disk is read, then the program can display the content to the screen. So, it makes sense to be able to use JavaScript for more directly OS-related tasks. That is exactly what Node.js does, by embracing the native and optimized use of events and event handlers in JavaScript, and by also using the very same JavaScript engine that is used in the Google Chrome browser (the V8 engine). We will come back to this later on.

At this point, we understand why:

3.2 | The paradigm

Events and events handlers are the fundamental concepts of a broader programming paradigm called event-driven programming. Event-driven programming is particularly well suited for programs that are input/output intensive. This is the case of web browsers and also of web servers (so of Node.js applications).

Event-driven programming existed long before JavaScript was created. Even if the event-driven architecture of the JavaScript runtime environments (JREs) differs from the runtime environments of other programming languages (Python, PHP, Ruby), the concept is not new at all. Also, it is definitely not specific to JavaScript. In particular, it is heavily used in OSs. For instance, OSs react to mouse and keyboards actions in similar ways browsers do. Under the hood, it is all events and event handlers.

Here are the key points to remember about event-driven programming:

3.3 | Events, event listeners and event handlers

You may have understood it by now, events are actions or occurrences that happen on the computer. The JRE (the web browser or Node.js) signals these events so that we can respond to them in some way, if desired. In other words, the JRE produces a signal of some kind when an event occurs, and provides a mechanism by which an action can be automatically taken into account when the event occurs.

Note: usually, in the web browser, we say that events are fired, and in Node.js, we usually say that events are emitted. Different terms, but the principle is the same.

In JavaScript, each event produced by the JRE can be handled by calling an event listener function. The event listeners usually takes 2 arguments:

It is important to understand that event listeners get directly called in our program so that event handlers can later be called, but not by us directly. We never directly invoke or call event handlers in our programs. We define them inside event listeners so that they can be automatically and internally called by the JRE. In other words, an event listener is a function that binds the name of an event (a String) to an event handler (a Function). By calling an event listener in such a way, we say that we are registering an event handler.

At a wedding, the event listener is the religious or administrative person that formalizes the marriage between the event and the event handler. By calling the event listener, we marry the event with its event handler, but we do not get to see what happens afterwards between them. They automatically and discretely do their thing.

Note: in the Node.js documentation, event handlers are called listeners. Also, the terms event listeners and event handlers are not universally defined, so you might see elsewhere that event handlers are referred as event listeners and vice versa.

Here is an example of an event listener in Node.js:

specialObject.on('TimeToEat', () => console.log('Pizza time!));

Here, we suppose that the specialObject can emit events. Among those events, it can emit the event named 'TimeToEat'. We call on this object the event listener function on(). We pass 2 arguments to this function:

  1. The 1st argument is the String 'TimeToEat', which is the name of the event we are listening to.
  2. The event handler, which is the function that will be automatically and internally called by Node.js when the event 'TimeToEat' is emitted. Note that we do not call directly the event handler, we just define it.

This is equivalent to this:

const eat = () => console.log('Pizza time!');
specialObject.on('TimeToEat', eat);

On the first line, we define the function eat. Then, we use this function as our event handler for the 2nd argument passed to the invocation of the function on().

3.4 | The major problem with events and event handlers

You may have noticed it by now, but events and event handlers are the source of a major problem. Between the moment a .js file is read and parsed by the JavaScript engine, and the moment the event is produced, there is usually an undefined amount of time. So the JRE must provide a mechanism to be able to read and parse the file it is executing once, from top to bottom, and still be able to invoke the event handlers later, when their corresponding events are produced.

This mechanism is the ability of the JavaScript engine to differentiate non-blocking functions that involve event handlers from blocking functions that do not. In other words, the JavaScript engine has a dual behavior: it can be either blocking or non-blocking, depending on the type of function it deals with.

Blocking and non-blocking functions are also referred to as synchronous and asynchronous functions. However, I prefer the terms blocking and non-blocking because they describe more precisely what happens under the hood in the JRE.

You will often see the expression "asynchronous JavaScript" to describe the fact that the JavaScript engine has a non-blocking behavior when dealing with non-blocking functions. This is quite misleading, and I discourage you to use the terms synchronous or asynchronous. This is radical but over the years, confusion has spread about the meaning of the word asynchronous. Asynchronous originally means not simultaneous or concurrent in time. In a sense, the expression asynchronous JavaScript would imply that the JavaScript engine could allow concurrent execution of different statements. This is not the case at all. Let us be clear: the JavaScript engine is synchronous by nature, due to its single-threaded architecture. To be crystal clear, the JavaScript engine can only execute one statement at a time, one after the other.

3.5 | Blocking and non-blocking functions

A JavaScript program is a sequence of statements that the JavaScript engine reads and executes one after the other, from top to bottom, in the exact order in which they appear in the file.

JavaScript has 6 kinds of statements:

  1. Declarations and/or assignments of variables.
  2. Declarations of functions.
  3. Increment, decrement and delete operations.
  4. Control structures (conditionals, loops and jumps).
  5. Calls or invocations of blocking functions.
  6. Calls or invocations of non-blocking functions.

For the first 5 kinds of statements, the JavaScript engine reads and executes one statement at a time, and also waits for the execution to be fully completed before moving on to the next statement. This behavior is called blocking behavior. No matter how long the execution takes to be fully completed, the JavaScript engine waits. Hence the use of the term "blocking". For instance, if the JavaScript engine reads and executes an infinite loop of synchronous code, it will never move on to the next statement, because it will be waiting for the current one to complete.

// An infinite loop blocks the execution of the code.
while (true) {

console.log('End of the loop');

In the previous code, the while statement gets continuously executed since the condition between the parenthesis always evaluates to true (in fact, it is the value true). Therefore, the last call of the console.log() function with the argument 'End of the loop' has no chance of being executed. The JavaScript engine waits for the while statement to complete, which never happens (it is an infinite loop).

But, there is one kind of statement that makes the JavaScript engine behave differently. It is the calls or invocations of non-blocking functions, i.e. the functions that use event handlers. When the JavaScript engine reads invocations of non-blocking functions, it "passes" these functions to the APIs of the JRE. In other words, it does not wait for the event(s) to be emitted nor for the event handlers to be executed. Instead, the JavaScript engine moves on to the next statement. Non-blocking functions do not block the JavaScript engine. The wait for the event to be emitted and the execution of the corresponding event handler are handled primarily by the other parts of the JRE. But the JavaScript engine does execute the event handlers when they are ready to be executed, and if they can be. We detail this in the next section.

3.6 | The heap, the call stack, the event loop and the queues

JavaScript Runtime Environment Source

Let us go a bit deeper on how a JRE handles the execution of blocking and non-blocking functions.

When the JavaScript engine parses the content of a file, statement by statement, from top to bottom, it will store any variable in an unstructured physical region of memory called the heap.

When the JavaScript engine parses a call or invocation of a blocking function, it pushes the function (referred in this context as a stack frame) into some kind of temporary waiting list called the call stack. The call stack can also be referred to as the execution stack, the program stack, the control stack, the run-time stack, the machine stack, or simply the stack. The call stack is a mechanism for the JavaScript engine to keep track of all of the pending subroutines (functions) to execute. The call stack is a "last in, first out" (LIFO) data structure that lists in real time the functions being executed. When a function is called, the JavaScript engine pushes the corresponding function on top of the call stack. When the function is completed (i.e. when the function returns), the function is popped off from the top of the call stack. When functions call other functions (nested function calls), the call stack starts piling up several functions, and it starts emptying down the call stack when the execution of each function is completed, one after the other.

When the JavaScript engine encounters a call or invocation of a non-blocking function (containing event handlers), the function is passed to the APIs of the JRE, and the JavaScript engine moves on. The adequate APIs handle the execution of the function. When the event handling functions are ready to be executed, they are passed by the APIs to other waiting lists usually referred to as queues. In Node.js, they are event queues and in web browsers, they are message queues.

The queues are "first in, first out" (FIFO) structures that list the event handling functions ready to be executed. The events handlers can only be executed by the call stack of the JavaScript engine, when the call stack is empty. The event loop is the mechanism of the JRE that checks if the call stack of the JavaScript engine is empty and empties the queues accordingly, by pushing the next event handler to the call stack for execution.

The event loop always gives priority to the call stack. That is why, in a JavaScript file, the blocking functions are always executed before the non-blocking functions. Event handling function can only be pushed to the call stack only when the call stack is empty, i.e. when all blocking functions have been executed.

3.7 | Message queues in web browsers

Even if this article is about Node.js, it cannot hurt to have more context about message queues.

In web browsers, there are 2 message queues (listed from highest to lowest priority):

  1. The job queue, which lists the non-blocking functions involving Promises.
  2. The callback queue, which lists the other non-blocking functions.

The functions in the job queue have higher priority than functions in the callback queue. So, in your JavaScript program, functions involving Promises will always be fully executed before any other non-blocking functions.

3.8 | Event queues in Node.js

In Node.js, non-blocking functions go through a more complex process, but schematically they are pushed into 5 event queues (listed from highest to lowest priority):

  1. The microtask queue , which lists non-blocking functions delayed with process.nextTick() or related to Promises.
  2. The timer queue, which lists the non-blocking functions calling any timer function of Node.js: setTimeout(), setInterval(), setImmediate(), clearTimeout(), clearImmediate() and clearInterval().
  3. The IO queue, which lists the non-blocking functions involving IO operations.
  4. The check queue (or the immediate queue), which lists the other non-blocking functions.
  5. The close queue, which lists the non-blocking functions related to close events operations.

3.9 | Non-blocking functions in Node.js

Node.js was designed and optimized for programs that are Input/Output intensive (I/O intensive), like highly concurrent servers that can handle many requests at the same time.

That is why Node.js is quite aggressive about its use of non-blocking functions. Almost all Node.js functions are non-blocking by default.

Also, Node.js was created before JavaScript had a Promise class, so non-blocking Node.js functions are callback-based. Generally, the last argument you pass to a non-blocking Node.js function is a callback function.

3.10 | Single-event functions and multi-events functions

In Node.js, we can distinguish 2 types of non-blocking functions.

  1. Single-event functions. Single-event functions, or error-first callback functions, are functions that emit implicitly a single event that has 2 outcomes: either an error or some data. These functions have an error-first callback function as their last parameter. This error-first callback function has itself 2 parameters. The first parameter, usually called error or err, will be assigned the value null if the event was successful, and will be assigned an Error object if the event raised an error. The second parameter, usually called data, will be assigned some data if the event was successful, and will be assigned null if the event raised an error. The reason for putting the error parameter first is to make it impossible for you to omit it.
  2. Multi-events functions. Multi-events functions emit several possible events that have predefined names (strings). These functions enable to bind an event name to an event handler. This type of functions is usually used for handling streaming data and they use Node.js special objects called Streams.

4 | Miscellaneous questions

What is the difference between the Node.js runtime environment and the Google Chrome runtime environment?

Even though these 2 runtime environments have the same JavaScript engine (V8), they do not have the same APIs. Also, their event loops are implemented differently: Node.js uses the libuv library for its event loop while Google Chrome uses the libevent library for its event loop.

What is there to know about V8?

Author: Dimitri Alamkan
Initial publication date:
Last updated: