Node.js 10.x released - What's NEW?

By: (plus.google.com) +David Herron; Date: April 24, 2018

Tags: Node.JS

After a year of development Node.js 10 has been released. It represents a huge step forward because of some high profile features and fixes. In October Node.js 10 will become the active Long Term Support branch, with 11.x becoming the new experimental branch. Let's take a look at what's been included.

Async/Await functions and ES6 Modules

For me these are the prominent features achieved in Node.js 10. I've spent the last 6+ months updating my book, Node.js Web Development, to the 4th edition, with the primary motivation being these two features. Which means that for the last 6 months I've been explaining to my future readers the importance of these two features. Please be understanding that I'm leading with this.

With the addition of ES6 modules in 9.x and async/await functions in 8.x, the adoption of ES2015/2016 features is pretty much complete. See the full status at (node.green) http://node.green/

The advantage of async functions is turning a "callback hell" situation like this:

router.get('/path/to/something', (req, res, next) => {
    doSomething(arg1, arg2, (err, data1) => {
       if (err) return next(err);
       doAnotherThing(arg3, arg2, data1, (err2, data2) => {
         if (err2) return next(err2);
         somethingCompletelyDifferent(arg1, arg42, (err3, data3) => {
           if (err3) return next(err3);
           doSomethingElse((err4, data4) => {
             if (err4) return next(err4);
             res.render('page', { data });
           });
         });
       });
    });
});

Into this:

router.get('/path/to/something', async (req, res, next) => {
    try {
        let data1 = await doSomething(req.query.arg1, req.query.arg2);
        let data2 = await doAnotherThing(req.query.arg3, req.query.arg2, data1);
        let data3 = await somethingCompletelyDifferent(req.query.arg1, req.query.arg42);
        let data4 = await doSomethingElse();
        res.render('page', { data1, data2, data3, data4 });
    } catch(err) {
        next(err); 
    }
});

Which would you rather maintain and debug?

The try/catch is required here because it is an Express router function, and these functions do not know an async function from a regular function. Instead you inform Express of errors with next(err), so we must use the try/catch block to make it work.

As for ES6 modules, the advantage is that all JavaScript programmers now share the same module format. ES6 modules work in both browsers and on Node.js. Well ...

Whether these modules or any other ES2015/2016/2017 feature works in browsers depends on the newness of the browser. Obviously there are still lots of old computers with old web browsers installed on which ES5 is the supported JavaScript.

In any case, what's an ES6 module look like?

var count = 0;
export function next() { return ++count; }
function squared() { return Math.pow(count, 2); }
export function hello() {
    return "Hello, world!";
}
export default function() { return count; }
export const meaning = 42;
export let nocount = -1;
export { squared };

Node.js has traditionally used CommonJS modules where things are exported by assigning them to the module.exports object. We have nearly 10 years experience with CommonJS modules in Node.js. The code snippet here shows several variations of ES6 module export syntax.

A thing is exported from a ES6 module with the export statement. The export default statement defines the default export from the module, while the others are all named exports.

If that module were named simple2.mjs then it could be used as so:

import * as simple2 from './simple2.mjs';
console.log(simple2.hello());
console.log(`${simple2.next()} ${simple2.squared()}`);
console.log(`${simple2.next()} ${simple2.squared()}`);
console.log(`${simple2.default()} ${simple2.squared()}`);
console.log(`${simple2.next()} ${simple2.squared()}`);
console.log(`${simple2.next()} ${simple2.squared()}`);
console.log(`${simple2.next()} ${simple2.squared()}`);
console.log(simple2.meaning);

And it executes like so:

$ node --experimental-modules simpledemo.mjs
(node:63937) ExperimentalWarning: The ESM module loader is experimental. Hello, world!
11
24
24
39
4 16
5 25
42

See: (nodejs.org) https://nodejs.org/dist/latest-v10.x/docs/api/esm.html

Promise.finally

The Promise object came with Node.js 4.x, and helped us begin our journey out of callback hell.

Initially Promise had two methods, then and catch to deal with whether a Promise resolved to a success state or rejection state. But there are other possible processing patterns that can be implemented on the Promise object. The continued use of the Bluebird library demonstrates that other needs are required. Promise.finally is one such need.

The idea here is a pattern similar to

try {
    some code
} catch (e) {
    handle errors
} finally {
    code that always executes whether or not an error is thrown
}

With Promise.finally we get:

let isLoading = true;

fetch(myRequest).then(function(response) {
    var contentType = response.headers.get("content-type");
    if(contentType && contentType.includes("application/json")) {
      return response.json();
    }
    throw new TypeError("Oops, we haven't got JSON!");
  })
  .then(function(json) { /* process your JSON further */ })
  .catch(function(error) { console.log(error); })
  .finally(function() { isLoading = false; });

In other words, you have code executing in then and catch as we expect currently, and the code in finally is then executed irregardless of errors.

Experimental promisified fs functions

Until now if you wanted sanity with fs module functions you had to use the fs-extra package. That 3rd party package offered a superset of the fs functions, and all its functions returned Promises rather than forcing you to use Callbacks.

We all want the convenience of async functions, which in turn requires widespread conversion from the Callback paradigm to the return-a-Promise paradigm. The Node.js baked-in modules include lots of functions using the Callback paradigm. Switching over to async functions requires switching all the backed-in modules to support returning Promises.

Included in the util module is a util.promisify function that aids with converting functions to return Promises.

Instead of require('fs') you use require('fs/promises'). From there you have the same API as the fs module, but the functions return Promises rather than forcing you to use Callbacks.

See: (nodejs.org) https://nodejs.org/dist/latest-v10.x/docs/api/fs.html#fs_fs_promises_api

HTTP/2 is now stable

HTTP/2 was developed from the earlier SPDY protocol. It supports a large number of improvements like pipelining of requests, compressing headers, multiplexing multiple requests over a single connection, and so on.

See: (nodejs.org) https://nodejs.org/api/http2.html

Improved cryptography

Node.js 10 includes an update to OpenSSL 1.1.0. This lets Node.js support more cipers and authenticators than before. The team expects to update to OpenSSL 1.1.1 before October, which will bring in support for TLS 1.3.

N-API eases burden on native code modules

An issue with native code Node.js modules has been API breakage between Node.js versions. With N-API, which is now non-experimental, that's a thing of the past. The ABI, Application Binary Interface, will remain the same from release to release.

One impact has been that running npm install resulted in a node_modules directory that worked only with a specific Node.js release. With N-API one could presumably use different platform releases on the same node_modules directory.

The N-API support is being back-ported to Node.js 6.x and 8.x release trains as well.

See: (nodejs.org) https://nodejs.org/dist/latest-v10.x/docs/api/n-api.html

Improved error messages

Currently the only way to distinguish one thrown Error from another is to inspect the strings. That leads to situations like:

try {
    execute buggy code
} catch (err) {
    if (err.message === 'This is th message') {
        do something
    } else {
        co something else
    }
}

In other words, we're matching strings, including typos, to distinguish errors. That's bad.

With the new error support we'd instead write:

try {
    execute buggy code
} catch (err) {
    if (err.code === 'ERR_INVALID_URL_SCHEME') {
        do something
    } else {
        co something else
    }
}

This is far more consistent and expressive.

See: (medium.com) https://medium.com/the-node-js-collection/node-js-errors-changes-you-need-to-know-about-dc8c82417f65

See: (nodejs.org) https://nodejs.org/dist/latest-v10.x/docs/api/errors.html

A new npm is promised soon

While Node.js 10.0 ships with npm 5.6.x, the team is promising npm 6.x to arrive soon.

This npm release includes a bunch of improvements.

A new command, npm audit, will incorporate checks for packages with known security problems.

Performance improvements are claimed.

See: (medium.com) https://medium.com/npm-inc/announcing-npm-6-5d0b1799a905

Node.js performance improvements

As always, Node.js releases include updates to the V8 engine, and Google is constantly moving it forward.

This time around they're working to improve performance of async functions and promises.