Node.js went from an out-of-the-box idea to a mainstay in record time. Today, it's a de facto standard for creating web applications, systems software, and more. Server-side Node frameworks like Express, build-chain tools like Webpack, and a host of utilities for every need make Node a hugely popular way to leverage the power and expressiveness of JavaScript on the back end.
Although Node now has competition from Deno and Bun, it remains the flagship JavaScript platform on the server.
Node owes much to JavaScript for its enormous popularity. JavaScript is a multiparadigm language that supports many different styles of programming, including functional programming, reactive programming, and object-oriented programming. It allows the developer to be flexible and take advantage of the various programming styles.
But JavaScript can be a double-edged sword. The multiparadigm nature of JavaScript means that nearly everything is mutable. Thus, you can’t brush aside the probability of object and scope mutation when writing Node.js code. Because JavaScript lacks tail-call optimization (which allows recursive functions to reuse stack frames for recursive calls), it’s dangerous to use recursion for large iterations. In addition to pitfalls like these, Node is single-threaded, so it’s imperative for developers to write asynchronous code. Node also suffers from facepalms common to all languages, like swallowing errors.
JavaScript can be a boon if used with care—or a bane if you are reckless. Following structured rules, design patterns, key concepts, and basic rules of thumb will help you choose the optimal approach to a problem. Which key concepts should Node.js programmers understand? Here are the 10 JavaScript concepts that I believe are most essential to writing efficient and scalable Node.js code.
JavaScript closures
A closure in JavaScript is an inner function that has access to its outer function's scope, even after the outer function has returned control. A closure makes the variables of the inner function private. Functional programming has exploded in popularity, making closures an essential part of the Node developer’s kit. Here's a simple example of a closure in JavaScript:
let count = (function () {
var _counter = 0;
return function () {return _counter += 1;}
})();
count();
count();
count();
>// the counter is now 3
The variable count is assigned an outer function. The outer function runs only once, which sets the counter to zero and returns an inner function. The _counter
variable can be accessed only by the inner function, which makes it behave like a private variable.
The example here is a higher-order function (or metafunction, a function that takes or returns another function). Closures are found in many other applications. A closure happens anytime you define a function inside another function and the inner function gets both its own scope and access to the parent scope—that is, the inner function can “see” the outer variables, but not vice versa.
This also comes in handy with functional methods like map(innerFunction)
, where innerFunction
can make use of variables defined in the outer scope.
JavaScript prototypes
Every JavaScript function has a prototype property that is used to attach properties and methods. This property is not enumerable. It allows the developer to attach methods or member functions to its objects. JavaScript supports inheritance only through the prototype property. In case of an inherited object, the prototype property points to the object’s parent. A common approach to attach methods to a function is to use prototypes as shown here:
function Rectangle(x, y) {
this.length = x;
this.breadth = y;
}
Rectangle.prototype.getDimensions = function () {
return { length : this._length, breadth : this._breadth };
};
Rectangle.prototype.setDimensions = function (len, bred) {
this.length = len;
this.breadth = bred;
};
Although modern JavaScript has pretty sophisticated class support, it still uses the prototype system under the hood. This is the source of much of the language’s flexibility.
Defining private properties using hash names
In the olden days, the convention of prefixing variables with an underscore was used to indicate that a variable was supposed to be private. However, this was just a suggestion and not a platform-enforced restriction. Modern JavaScript offers hashtag private members and methods for classes:
class ClassWithPrivate {
#privateField;
#privateMethod() { }
}
Private hash names is a newer and very welcome feature in JavaScript! Recent Node versions and browsers support it, and Chrome devtools lets you directly access private variables as a convenience.
Defining private properties using closures
Another approach that you will sometimes see for getting around the lack of private properties in JavaScript’s prototype system is using a closure. Modern JavaScript lets you define private properties by using the hashtag prefix, as shown in the above example. However, this does not work for the JavaScript prototype system. Also, this is a trick you will often find in code and its important to understand what it is doing.
Defining private properties using closures lets you simulate a private variable. The member functions that need access to private properties should be defined on the object itself. Here's the syntax for making private properties using closures:
function Rectangle(_length, _breadth) {
this.getDimensions = function () {
return { length : _length, breadth : _breadth };
};
this.setDimension = function (len,bred) {
_length = len;
_breadth = bred
};
}
JavaScript modules
Once upon a time, JavaScript had no module system, and developers devised a clever trick (called the module pattern) to rig up something that would work. As JavaScript evolved, it spawned not one but two module systems: the CommonJS include
syntax and the ES6 require
syntax.
Node has traditionally used CommonJS, while browsers use ES6. However, recent versions of Node (in the last few years) have also supported ES6. The trend now is to use ES6 modules, and someday we’ll have just one module syntax to use across JavaScript. ES6 looks like so (where we export a default module and then import it):
// Module exported in file1.js…
export default function main() { }
// …module imported in file2.js
import main from "./file1";
You’ll still see CommonJS, and you’ll sometimes need to use it to import a module. Here's how it looks to export and then import a default module using CommonJS:
// module exported in file1.js…
function main = () { }
module.exports = main;
// …module imported in file2.js
Error handling
No matter what language or environment you are in, error handling is essential and unavoidable. Node is no exception. There are three basic ways you’ll deal with errors: try/catch blocks, throwing new errors, and on()
handlers.