Node.js 10: Important Changes

Node.js 10: Important Changes

The recent release of the node.js is a major milestone in its development. It contains many changes in the library, bugfixes and updated v8 engine. the complete changelog is here. This is a review of new features. It also inclues changes that have impact on the backward compatibility.

Assert

Calling assert.fail() with more than one argument is deprecated.

Effect on compatibility: low

From now on calling assert.fail with more than one argument shows the deprecation warning (node:66308) [DEP0094] DeprecationWarning: assert.fail() with more than one argument is deprecated. Please use assert.strictEqual() instead or only pass a message.

> require('assert').fail(1, 2);
AssertionError [ERR_ASSERTION]: 1 != 2
> (node:66842) [DEP0094] DeprecationWarning: assert.fail() with
more than one argument is deprecated. Please use assert.strictEqual()
instead or only pass a message.

It definitely makes assert.fail more consistent with other frameworks (see NUnit's Assert.Fail).

Calling assert.ok() with no arguments will now throw.

Effect on compatibility: low

When assert.ok() is called without arguments, it throws AssertionError [ERR_ASSERTION]: No value argument passed to assert.ok().

> require('assert').ok();
AssertionError [ERR_ASSERTION]: No value argument passed to `assert.ok()`

assert.ok does not really have any meaning without argument and most likely indicate either incomplete assertion or wrong assumption on how it works. But test runner actually may catch this exception and consider test successful. NUnit's Assert.Pass is for similar reason, for example.

In the previous version of the node.js it throws as well, but with different error message:

9.7.1> require('assert').ok();
AssertionError [ERR_ASSERTION]: undefined == true

Calling assert.ifError() will now throw with any argument other than undefined or null.

Effect on compatibility: moderate

assert.ifError is useful when asserting error in callbacks.

> require('fs').open('asdf', 'r', require('assert').ifError);
undefined
> AssertionError [ERR_ASSERTION]: ifError got unwanted exception:
ENOENT: no such file or directory, open 'asdf'
    at FSReqWrap.oncomplete (fs.js:136:20)

Previously assert.isError(e) throws only when e is of Error type. Now it throws for everything except undefined and null.

9.7.1> require('assert').ifError(1);
undefined
10.0.0> require('assert').ifError(1);
AssertionError [ERR_ASSERTION]: ifError got unwanted exception: 1

That's a quite significant change. Some tests may fail.

The assert.rejects() and assert.doesNotReject() methods have been added.

Effect on compatibility: none

These two are to check whether a Promise rejects or not.

> require('assert').rejects(Promise.resolve());
...
> (node:66842) UnhandledPromiseRejectionWarning:
AssertionError [ERR_ASSERTION]: Missing expected rejection.

> require('assert').doesNotReject(Promise.reject());
...
> (node:66842) UnhandledPromiseRejectionWarning:
AssertionError [ERR_ASSERTION]: Got unwanted rejection.
Actual message: "undefined"

It worth take a look on the docs for the assert.reject and assert.doesNotReject.

Buffer

Uses of new Buffer() and Buffer() outside of the node_modules directory will now emit a runtime deprecation warning.

Effect on compatibility: low

This warning targets thus developers, who creating scripts. Moduled included from the node_modules folder should not produce the warning.

> new Buffer([]);
<Buffer >
> (node:67301) [DEP0005] DeprecationWarning: Buffer() is
deprecated due to security and usability issues. Please use
the Buffer.alloc(), Buffer.allocUnsafe(), or Buffer.from()
methods instead.

The main reason for this change (as I understood it) is that using one single constructor for initialising makes it too complicated, therefore a) error-prone; b) complicated in both describing and usage.

A bit more on the reasoning behind this change is in the doc.

Buffer.isEncoding() now returns undefined for falsy values, including an empty string.

Effect on compatibility: moderate

It rather looks like a regression.

9.7.1> Buffer.isEncoding('');
true
10.0.0> Buffer.isEncoding('');
false

Buffer.fill() will throw if an attempt is made to fill with an empty Buffer.

Effect on compatibility: low

Any of the following code lines makes older version of the node runs forever, make sure that you do not have anything like this in your production code:

9.7.1> Buffer.alloc(1).fill(Buffer.alloc(0));
9.7.1> Buffer.alloc(1).fill(new Buffer(0));
9.7.1> Buffer.alloc(1).fill(new Buffer([]));

Although filling with zeroes might be an option in this case, the team has decided consider it illegal operation and throw an error.

10.0.0> Buffer.alloc(1).fill(Buffer.alloc(0));
TypeError [ERR_INVALID_ARG_VALUE]: The argument 'value' is invalid.
Received <Buffer >
    at _fill (buffer.js:890:13)
    at Buffer.fill (buffer.js:828:10)

Child Process

Undefined properties of env are ignored.

Effect on compatibility: major

Previously the environment variables with undefined values in env are serialised to the string 'undefined'. It was a bit controversial and now such variables are ignored. It may cause behaviour change of the called program.

9.7.1> require('child_process').exec('echo $MAGIC', { env : { MAGIC : undefined } }, console.log);0;
null 'undefined\n' ''

10.0.0> require('child_process').exec('echo $MAGIC', { env : { MAGIC : undefined } }, console.log);0;
null '\n' ''

Console

The console.table() method has been added.

Effect on compatibility: none

I belive that need for this feature is driven by the console.table method from WebAPI. The behaviour is similar: the good-looking table that makes easier to analyse tabular data.

> console.table({ firstName : "John", lastName : "Smith" });
┌───────────┬─────────┐
│  (index)  │ Values  │
├───────────┼─────────┤
│ firstName │ 'John'  │
│ lastName  │ 'Smith' │
└───────────┴─────────┘

Unfortunately, it does not autotrim values so make sure that there is enough room in the console.

> console.table(['1'.repeat(40)])
┌─────────┬─────────────────────────────────────────
───┐
│ (index) │                   Values                
   │
├─────────┼─────────────────────────────────────────
───┤
│    0    │ '111111111111111111111111111111111111111
1' │
└─────────┴─────────────────────────────────────────
───┘

Crypto

The crypto.createCipher() and crypto.createDecipher() methods have been deprecated.

Effect on compatibility: none

Instead of the crypto.createCipher() and crypto.createDecipher() it is recommended to use the crypto.createCipheriv() and crypto.createDecipheriv() methods.

The problem with auto-generated initialisation vector is that reusing this vector leads to the same encrypted sequence of bytes. Ideally, iv should be different even when the code and data are the same.

These deprecations are on the documentation level only (no messages will be printed to stdout or stderr).

The crypto.DEFAULT_ENCODING property has been deprecated.

Effect on compatibility: low

Using global variables is a bad practice. No doubt that relying on this property complicates reusability of the libraries. Changing it may lead to the number of issues that is very hard to detect.

> crypto.DEFAULT_ENCODING;
'buffer'
> (node:69516) [DEP0091] DeprecationWarning:
crypto.DEFAULT_ENCODING is deprecated.

The ECDH.convertKey() method has been added.

Effect on compatibility: none

New method for converting the Elliptic Curve Diffie-Hellman public key from one format to another. Might be useful if you are into cryptography.

The crypto.fips property has been deprecated.

Effect on compatibility: low

When crypto.fips is true then the a FIPS compliant crypto provider is currently in use. Usages of this property can be replaced with crypto.getFips() and crypto.setFips().

Although this property is rarely in use, such kind of deprecation is an example of what should be avoided to support ESM modules.

Dependencies

V8 has been updated to 6.6.

Effect on compatibility: moderate

V8 JavaScript engine was updated to 6.6.346.23 from 6.5.254.43.

The list of V8 changes can be found here.

In brief, there are some changes in the language:

9.7.1> (function /* hi */ foo() {}).toString();
'function foo() {}'

10.0.0> (function /* hi */ foo() {}).toString();
'function /* hi */ foo() {}'

9.7.1> try { console.log('hi'); } catch { }
try { console.log(); } catch { }
                             ^
SyntaxError: Unexpected token {

10.0.0> try { console.log('hi'); } catch { }
hi

OpenSSL has been updated to 1.1.0h.

Effect on compatibility: none

There are some changes and security vulnerabilities fixed. See detailed changelog for OpenSSL 1.1.0h here.

EventEmitter

The EventEmitter.prototype.off() method has been added.

Effect on compatibility: none

The new method off() is just an alias for removeListener() and presumably was added to complete the method on(), which adds new listener to the event emitter.

> const e = new (require('events').EventEmitter)();
> const h = () => console.log('event handler');
> e.on('evt', h).emit('evt');
event handler
true
> e.off('evt', h).emit('evt');
false

File System

The fs/promises API.

Effect on compatibility: none

Finally, major step towards native support for promises in node.js. Now the fs methods can be called with await:

> (async () => console.log(await require('fs/promises').readdir('.')))();
...
(node:70521) ExperimentalWarning: The fs/promises API is experimental
[ '.DS_Store',
  '.Trash',
  '.bash_history',
  '.bash_profile',
  '.bash_sessions',
  ... ]

However, it should be used with caution as the API is experimental so it may be changed or removed.

More details on it here.

Invalid path errors are now thrown synchronously.

Effect on compatibility: low

Differently from the previous version of node.js, it is possible to catch the exception of the filesystem functions when the path is incorrect. Also path check is performed a bit differently.

9.7.1> try { fs.readdir('\u0000'); } catch (e) {}
Process crashed with: Error: ENOENT: no such file or
directory, scandir ''

9.7.1> fs.readdir('\u0000', () => { });
undefined

10.0.0> fs.readdir('\u0000', () => { });
TypeError [ERR_INVALID_ARG_VALUE]: The argument 'path'
must be a string or Uint8Array without null bytes. ...

10.0.0> try { fs.readdir('\u0000', () => { }); } catch {}
undefined

The fs.readFile() method now partitions reads.

Effect on compatibility: very low

This is an important performance improvement. The readFile() method was modified to read files in chunks, giving the place for another callbacks.

HTTP

Processing of HTTP Status codes 100, 102-199 has been improved.

Effect on compatibility: low

The HTTP statuses 100, 102-199 are now processed according to RFCs. New event 'information' has been added to the http.ClientRequest. This event is emitted when the server sends a 1xx response (excluding 101 Upgrade).

*Multi-byte characters in URL paths are now forbidden.

Effect on compatibility: major

URL path is a part of the URL after the domain name and before the query parameters. For example, in the URL https://alexatnet.com/node-js-10-important-changes/ the path is '/node-js-10-important-changes/'.

Previously the http.request() method strips the higher bytes from multi-byte characters when generating the request.

9.7.1> http.request({host: "example.com", port: "80", path: "/N"});
ClientRequest { ... }

10.0.0> http.request({host: "example.com", port: "80", path: "/N"});
TypeError [ERR_UNESCAPED_CHARACTERS]: Request path contains
unescaped characters

Net

The 'close' event will be emitted after 'end'.

Effect on compatibility: low

This change fixes the order of events in Socket. In previous version end may come after close, which is incorrect. In the new version the end always goes before close.

Perf_hooks

The PerformanceObserver class is now an AsyncResource.

Effect on compatibility: none

The PerformanceOserver class observes new entries in the performance timeline. Making it AsyncResource allows to use async_hooks to monitor it.

This example shows how to use async_hooks and PerformanceOserver: Measuring the duration of async operations.

Trace events are now emitted for performance events.

Effect on compatibility: none

There are two new trace categories to track: node.perf.usertiming and node.perf.timerify. See more on the tracing here.

The performance API has been simplified.

Effect on compatibility: low

The methods performance.getEntries*() and performance.clear*() are removed in the performance API. As this API is experimental, it is expected that it might be changed.

*Performance milestone marks will be emitted as trace events. *

Effect on compatibility: none

Now it is possible to track the process of node.js bootstrapping. The new category node.bootstrap was added with the following names: nodeStart, v8Start, loopStart, loopExit, bootstrapComplete, thirdPartyMainStart, thirdPartyMainEnd, clusterSetupStart, clusterSetupEnd, moduleLoadStart, moduleLoadEnd, preloadModulesLoadStart, preloadModulesLoadEnd.

Process

Using non-string values for process.env is deprecated.

Effect on compatibility: none

When setting a property of the process.env, it gets converted to string. It happens for the undefined and null values as well.

> process.env.TEST = undefined;
1
> console.log(typeof process.env.TEST, process.env.TEST);
string undefined

This behaviour is deprecated, but in docs only.

REPL

REPL supports top-level await

Effect on compatibility: none

If node is started with --experimental-repl-await flag, it adds experimental support for top-level await:

$ node --experimental-repl-await
> await Promise.resolve(1);
1

Without this flat attempt to use await on the top level causes the syntax error:

$ node
> await Promise.resolve(1);
await Promise.resolve(1);
^^^^^
SyntaxError: await is only valid in async function

Proxy objects are shown as Proxy objects when inspected.

Effect on compatibility: none

In the previous version of the node the process crashes with the error when the proxy method throws:

9.7.1> new Proxy({}, { get(t, k, r) { throw new Error(); } });
Process crashed with: Error
    at Object.get (evalmachine.<anonymous>:1:37)
    ...

With the new version the inpected object is displayed as proxy:

10.0.0> new Proxy({}, {get(t, k, r) { throw new Error(); }});
Proxy [ {}, { get: [Function: get] } ]

Streams

The 'readable' event is now always deferred with nextTick.

Effect on compatibility: low

Consider the example when the custom readable stream in its _read method asynchronously pushes several times (based on this example):

class TestStream extends require('stream').Readable {
  constructor(options) { super(options); }
  _read (n) { setTimeout(() => { this.push('x'); this.push('x'); }, 1000); }
}
new TestStream().pipe(process.stdout);

In the previous node version every push will immediately call _read, adding more handlers. In the new version the push schedules _read to the next tick and call it only once.

The pipeline() method has been added.

Effect on compatibility: none

The pipeline() method forwards the data and errors through the chain of streams, calling callback when they are complete.

const pipeline = require('util').promisify(require('stream').pipleline);
await pipeline(
  ... input stream such as fs.createReadStream() ...,
  ... encode, zip, or other streams transformations ...,
  ...,
  ... output stream such as fs.createWriteStream() or HTTP response ...)

Experimental support for async for-await has been added to stream.Readable.

Effect on compatibility: none

It is now possible to read the content of the readable stream by using for ... of loop. Create the test.js file with the content

(async () => {
  for await (const chunk of require('fs').createReadStream('test.js')) {
    process.stdout.write(chunk);
  }
})();

and run with node test.js:

$ node test.js 
(node:73267) ExperimentalWarning: Readable[Symbol.asyncIterator]
is an experimental feature. This feature could change at any time
(async () => { ... skipped ... })();

Trace Events

A new trace_events top-level module allows trace event categories to be enabled/disabled at runtime.

Effect on compatibility: none

This module controls the traced event categories at runtime. The entire module is new and added in node 10. See docs here.

URL

The WHATWG URL API is now a global.

Effect on compatibility: none

The URL anc URLSearchParams classes are now global, just like in the browsers.

> new URL('http://alexatnet.com/')
URL {
  href: 'http://alexatnet.com/',
  ...
}

Util

util.types.is[...] type checks have been added.

There is a number of deprecated methods (since 4.0.0) in the util module such as isArray, isString, etc. But checking for the types is so useful so similar set of methods is added again, now as util.types.is[...] methods. The complete list is: isAnyArrayBuffer, isArgumentsObject, isArrayBuffer, isAsyncFunction, isBooleanObject, isDataView, isDate, isExternal, isFloat32Array, isFloat64Array, isGeneratorFunction, isGeneratorObject, isInt8Array, isInt16Array, isInt32Array, isMap, isMapIterator, isNativeError, isNumberObject, isPromise, isProxy, isRegExp, isSet, isSetIterator, isSharedArrayBuffer, isStringObject, isSymbolObject, isTypedArray, isUint8Array, isUint8ClampedArray, isUint16Array, isUint32Array, isWeakMap, isWeakSet, isWebAssemblyCompiledModule.

> (function () { return require('util').types.isArgumentsObject(arguments); })();
true

Support for bigint formatting has been added to util.inspect().

To test this feature the flag --harmony-bigint should be present in node's command line arguments. util.inspect() will then output the bigints correctly:

> require('util').inspect(12345678901234567890n);
'12345678901234567890n'