Advancing Node.js with ECMAScript 2015, 2016, 2017, and beyond

In 2015, the ECMAScript committee released a long-awaited major update of the JavaScript language. The update brought in many new features to JavaScript, such as Promises, arrow functions, and class objects. The language update sets the stage for improvement since it should dramatically improve our ability to write clean, understandable JavaScript code.

The browser makers are adding those much-needed features, meaning the V8 engine is adding those features as well. These features are making their way to Node.js, starting with version 4.x.

By default, only the ES2015, 2016, and 2017 features that V8 considers stable are enabled by Node.js. Further features can be enabled with command-line options. The almost-complete features are enabled with the –es_staging option. The website documentation gives more information.

The ES2015 (and later) features make a big improvement to the JavaScript language. One feature, the Promise class, should mean a fundamental rethinking of common idioms in Node.js programming. In ES2017, a pair of new keywords, async and await, simplifies writing asynchronous code in Node.js, which should encourage the Node.js community to further rethink the common idioms of the platform.

There’s a long list of new JavaScript features but let’s quickly go over the two of them that we’ll use extensively.

The first is a lighter-weight function syntax called the arrow function:

fs.readFile(‘file.txt’, ‘utf8’, (err, data) => {

if (err) …; // do something with the error

else …; // do something with the data

});

This is more than the syntactic sugar of replacing the function keyword with the fat arrow. Arrow functions are lighter weight as well as being easier to read. The lighter weight comes at the cost of changing the value of this inside the arrow function. In regular functions, this has a unique value inside the function. In an arrow function, this has the same value as the scope containing the arrow function. This means that, when using an arrow function, we don’t have to jump through hoops to bring this into the callback function because this is the same at both levels of the code.

The next feature is the Promise class, which is used for deferred and asynchronous computations. Deferred code execution to implement asynchronous behavior is a key paradigm for Node.js and it requires two idiomatic conventions:

  • The last argument to an asynchronous function is a callback function, which is called when an asynchronous execution is to be performed.
  • The first argument to the callback function is an error indicator.

While convenient, these conventions have resulted in multilayer code pyramids that can be difficult to understand and maintain:

doThis(arg1, arg2, (err, result1, result2) => {

if (err) …;

else {

// do some work

doThat(arg2, arg3, (err2, results) => {

if (err2) …;

else {

doSomethingElse(arg5, err => {

if (err) .. ;

else ..;

}

});

}

});

});

You don’t need to understand the code; it’s just an outline of what happens in practice as we use callbacks. Depending on how many steps are required for a specific task, a code pyramid can get quite deep. Promises will let us unravel the code pyramid and improve reliability because error handling is more straightforward and easily captures all errors.

A Promise class is created as follows:

function doThis(arg1, arg2) {

return new Promise((resolve, reject) => {

// execute some asynchronous code

if (errorIsDetected) return reject(errorObject);

// When the process is finished call this: resolve(result1, result2);

});

}

Rather than passing in a callback function, the caller receives a Promise object. When properly utilized, the preceding pyramid can be coded as follows:

doThis(arg1, arg2)

.then(result => {

// This can receive only one value, hence to

// receive multiple values requires an object or array

return doThat(arg2, arg3);

})

.then((results) => {

return doSomethingElse(arg5);

})

.then(() => {

// do a final something

})

.catch(err => {

// errors land here

});

This works because the Promise class supports chaining if a then function returns a Promise object.

The async/await feature implements the promise of the Promise class to simplify asynchronous coding. This feature becomes active within an async function:

async function mumble() {

// async magic happens here

}

An async arrow function is as follows:

const mumble = async () => {

// async magic happens here

};

To see how much of an improvement the async function paradigm gives us, let’s recode the earlier example as follows:

async function doSomething(arg1, arg2, arg3, arg4, arg5) {

const { result1, result2 } = await doThis(arg1, arg2);

const results = await doThat(arg2, arg3);

await doSomethingElse(arg5);

// do a final something return finalResult;

}

Again, we don’t need to understand the code but just look at its shape. Isn’t this a breath of fresh air compared to the nested structure we started with?

The await keyword is used with a Promise. It automatically waits for the Promise to resolve. If the Promise resolves successfully, then the value is returned and if it resolves with an error, then that error is thrown. Both handling results and throwing errors are handled in the usual manner.

This example also shows another ES2015 feature: destructuring. The fields of an object can be extracted using the following code:

const { value1, value2 } = {

value1: “Value 1”, value2: “Value 2”, value3: “Value3”

};

This demonstrates having an object with three fields but only extracting two of the fields.

To continue our exploration of advances in JavaScript, let’s take a look at Babel.

1. Using Babel to use experimental JavaScript features

The Babel transpiler is the leading tool for using cutting-edge JavaScript features or experimenting with new JavaScript features. Since you’ve probably never seen the word transpiler, it means to rewrite source code from one language to another. It is like a compiler in that Babel converts computer source code into another form, but instead of directly executable code, Babel produces JavaScript. That is, it converts JavaScript code into JavaScript code, which may not seem useful until you realize that Babel’s output can target older JavaScript releases.

Put more simply, Babel can be configured to rewrite code with ES2015, ES2016, ES2017 (and so on) features into code conforming to the ES5 version of JavaScript. Since ES5 JavaScript is compatible with practically every web browser on older computers, a developer can write their frontend code in modern JavaScript then convert it to execute on older browsers using Babel.

The Node Green website makes it clear that Node.js supports pretty much all of the ES2015, 2016, and 2017 features. Therefore, as a practical matter, we no longer need to use Babel for Node.js projects. You may possibly be required to support an older Node.js release and you can use Babel to do so.

For web browsers, there is a much longer time lag between a set of ECMAScript features and when we can reliably use those features in browser-side code. It’s not that the web browser makers are slow in adopting new features as the Google, Mozilla, and Microsoft teams are proactive about adopting the latest features. Apple’s Safari team seems slow to adopt new features, unfortunately. What’s slower, however, is the penetration of new browsers into the fleet of computers in the field.

Therefore, modern JavaScript programmers need to familiarize themselves with Babel.

To get a brief introduction to Babel, we’ll use it to transpile the scripts we saw earlier to run on Node.js 6.x. In those scripts, we used async functions, a feature that is not supported on Node.js 6.x.

In the directory containing ls.js and ls2.js, type these commands:

$ npm install babel-cli \

babel-plugin-transform-es2015-modules-commonjs \

babel-plugin-transform-async-to-generator 

This installs the Babel software, along with a couple of transformation plugins. Babel has a plugin system so that you can enable the transformations required by your project. Our primary goal in this example is converting the async functions shown earlier into Generator functions. Generators are a new sort of function introduced with ES2015 that form the foundation for the implementation of async functions.

Because Node.js 6.x does not have either the fs.promises function or util.promisify, we need to make some substitutions to create a file named ls2- old-school.js:

const fs = require(‘fs’);

const fs_readdir = dir => {

return new Promise((resolve, reject) => {

fs.readdir(dir, (err, fileList) => {

if (err) reject(err);

else resolve(fileList);

});

});

};

async function listFiles() {

try {

let dir = ‘.’;

if (process.argv[2]) dir = process.argv[2];

const files = await fs_readdir(dir);

for (let fn of files) {

console.log(fn);

}

} catch(err) { console.error(err); }

}

listFiles();

We have the same example we looked at earlier, but with a couple of changes. The fs_readdir function creates a Promise object then calls fs.readdir, making sure to either reject or resolve the Promise based on the result we get. This is more or less what the util.promisify function does.

Because fs_readdir returns a Promise, the await keyword can do the right thing and wait for the request to either succeed or fail. This code should run as is on Node.js releases, which support async functions. But what we’re interested in—and the reason why we added the fs_readdir function—is how it works on older Node.js releases.

The pattern used in fs_readdir is what is required to use a callback-oriented function in an async function context.

Next, create a file named .babelrc, containing the following:

{

“plugins”: [

“transform-es2015-modules-commonjs”,

“transform-async-to-generator”

]

}

This file instructs Babel to use the named transformation plugins that we installed earlier. As the name implies, it will transform the async functions to generator functions.

Because we installed babel-cli, a babel command is installed, such that we can type the following:

$ ./node_modules/.bin/babel -help

To transpile your code, run the following command:

$ ./node_modules/.bin/babel ls2-old-school.js -o ls2-babel.js 

This command transpiles the named file, producing a new file. The new file is as follows:

‘use strict’;

function _asyncToGenerator(fn) { return function ()

{ var gen = fn.apply(this, arguments);

return new Promise(function (resolve, reject)

{ function step(key, arg) { try { var info =

gen[key](arg); var value = info.value; } catch (error)

{ reject(error); return; } if (info.done) { resolve(value);

} else { return Promise.resolve(value).then(function (value)

{ step(“next”, value); }, function (err) { step(“throw”,

err); }); } } return step(“next”); }); }; }

const fs = require(‘fs’);

const fs_readdir = dir => {

return new Promise((resolve, reject) => {

fs.readdir(dir, (err, fileList) => {

if (err) reject(err);

else resolve(fileList);

});

});

};

_asyncToGenerator(function* () {

var dir = ‘.’;

if (process.argv[2]) dir = process.argv[2];

const files = yield fs_readdir(dir);

for (let fn of files) {

console.log(fn);

}

})().catch(err => {

console.error(err);

});

This code isn’t meant to be easy to read for humans. Instead, it means that you edit the original source file and then convert it for your target JavaScript engine. The main thing to notice is that the transpiled code uses a Generator function (the notation function* indicates a generator function) in place of the async function and the yield keyword in place of the await keyword. What a generator function is—and precisely what the yield keyword does—is not important; the only thing to note is that yield is roughly equivalent to await and that the _asyncToGenerator function implements functionality similar to async functions. Otherwise, the transpiled code is fairly clean and looks rather similar to the original code.

The transpiled script is run as follows:

$ nvm use 4

Now using node v4.9.1 (npm v2.15.11)

$ node –version v4.9.1

$ node ls2-babel

.babelrc app.js ls.js

ls2-babel.js

ls2-old-school.js ls2.js node_modules 

In other words, it runs the same as the async version but on an older Node.js release. Using a similar process, you can transpile code written with modern ES2015 (and so on) constructions so it can run in an older web browser.

In this section, we learned about advances in the JavaScript language, especially async functions, and then learned how to use Babel to use those features on older Node.js releases or in older web browsers.

Source: Herron David (2020), Node.js Web Development: Server-side web development made easy with Node 14 using practical examples, Packt Publishing.

Leave a Reply

Your email address will not be published. Required fields are marked *