Node.js 15 Is Out! What Does It Mean for You?
The Node.js team has announced the release of a new major version — Node.js 15 🎉!
While a new release is always exciting, some folks are wondering what it means for them.
How do the changes affect me, and what should I do before updating?
What are the new features and their practical use cases?
Aside from a single, but important, breaking change, Node.js 15 is mainly about new features. Updating from older Node.js versions should therefore be fairly straightforward. Keep in mind that Node.js 15 won't go into LTS, more on that below.
Read on to find out what the new features are and how you can use them in your projects.
Unhandled rejections are thrown
Prior to Node.js 15, you would get the following error when a promise would reject without being caught anywhere in the promise chain:
(node:1309) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch().
(node:1309) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
You're probably familiar with this message. This warning has been around since Node.js 6.6, released over 4 years ago. The team behind Node.js has finally decided it was time to act on the deprecation warning.
From version 15 and onwards, Node.js will raise an uncaught exception and terminate the application. This can be a nasty surprise in production if you decide to update Node.js without being aware of this change.
By adding a global handler of the unhandledRejection
event, you can catch unhandled rejections and decide how you want to proceed. The following code will simply log the event and keep the application running, similar to the behaviour of earlier Node.js versions:
// Global handler for unhandled promise rejections
process.on('unhandledRejection', (reason, promise) => {
console.log('Unhandled Rejection at:', promise, 'reason:', reason);
// Decide whether to:
// 1. Do nothing and keep the application running or
// 2. Exit with `process.exit(1)` and let a process manager automatically restart the application
});
The recommended approach is to let the application crash since it might be in a faulty state that might lead to more errors and weird bugs. If you decide to crash, make sure you're using a process manager that will automatically restart your application.
It's best to handle a rejection as close as possible to the place it was thrown. I tend to treat the unhandledRejection
handler as the last resort and strive to have no unhandled rejections in my apps. You can set up alerts in production and track down unhandled rejections that you forgot to catch in your code.
New language features with V8 8.6
Node.js 15 upgrades V8 from 8.4 to 8.6 which brings some exciting new language features.
Logical assignment operators
In the history of ECMAScript, we've seen multiple times where the assignment operator was combined with other operators to create shorter expressions of commonly used combinations. In JavaScript, if you want to add 5
to a variable x
you can shorten x = x + 5
to x += 5
.
Similarly, the new logical assignment operators are a combination of the logical operators (&&
, ||
and ??
) and the assignment operator =
. Here are some examples of how you would write things before and after these new operators:
/**
* Logical AND assignment (&&=)
*/
// Old
if (x) {
x = y;
}
// Old
x && (x = y);
// New
x &&= y;
/**
* Logical OR assignment (||=)
*/
// Old
if (!x) {
x = y;
}
// Old
x || (x = y);
// New
x ||= y;
/**
* Nullish coalescing assignment (??=)
*/
// Old
if (x === null || x === undefined) {
x = y;
}
// Old
x ?? (x = y);
// New
x ??= y;
A recommended read is this V8 blog post with an in-depth explanation of logical assignment operators. I just learned that x && (x = y)
is not the same as x = x && y
, although in both cases x
will always have the same value!
String.prototype.replaceAll()
I like to use .replace()
with a substring parameter whenever I can. It's clear what it does and saves me from having to use Regex. However, whenever I needed to replace all occurrences of a substring, not just the first one, I had to resort to Regex to get the job done.
Not anymore! .replaceAll(substring, replacement)
does exactly what the name implies. It searches a string for all occurrences of a substring and replaces them with the replacement.
// Old 🙅🏻♀️
'q=query+string+parameters'.replace(/\+/g, ' ');
// 'q=query string parameters'
// New 🎉, using `.replaceAll()`
'q=query+string+parameters'.replaceAll('+', ' ');
// 'q=query string parameters'
Promise.any()
With the addition of Promise.any()
, there is one reason less to use Bluebird (that's if you're still using it these days). Promise.any()
takes an array of promises and returns as soon as the first promise fulfils.
This might be useful when you need to query a resource that might be present in several stores but you don't care from which store it originates. You query all stores and respond to the client as soon as the first query returns.
try {
const numberOfLikes = await Promise.any([
queryLikesFromDB(),
queryLikesFromCache(),
]);
// Any of the promises were fulfiled
console.log(numberOfLikes);
} catch (error) {
// All promises were rejected, log rejection values
console.log(error.errors);
}
AbortController
Speaking of Bluebird, Node.js 15 comes with an experimental implementation of AbortController
based on the AbortController Web API. It facilitates native promise cancellation which has been a long-discussed feature and it's nice to see progress being made on this front.
If you're using node-fetch
, the recommended way to timeout a request is as follows:
const fetch = require('node-fetch');
const controller = new AbortController();
// Abort after 5 seconds
setTimeout(() => controller.abort(), 5000);
try {
// Pass the signal to fetch so it can listen to the abort event
const response = await fetch('https://example.com', { signal: controller.signal });
// Do something with the response..
} catch (error) {
if (error.name === 'AbortError') {
console.log('Request was aborted');
}
}
Only a select number of promise-based APIs support cancellation with AbortController
at the moment. The list will surely expand as the feature matures into stability.
Native promise API for setTimeout
If you have fully adopted promises and async/await in your codebase, setTimeout
is one of the last places where you still have to use the callback pattern:
console.log('Starting async operation..');
setTimeout(() => {
console.log('Async done!');
}, 1000);
You might have promisified setTimeout with the util
module, or used a 3rd party library (e.g.: delay
, p-sleep
) as an alternative. With Node.js 15 and up, you can replace these workarounds with a native solution.
Node.js 15 ships with the Timers Promises API which has a promisified version of setTimeout
:
const { setTimeout } = require('timers/promises');
console.log('Starting async operation..');
await setTimeout(1000);
console.log('Async done!');
NPM 7
A new Node.js version usually means a new NPM version is shipped along by default. Node.js 15 comes with a major upgrade of NPM.
NPM 7 introduces several notable features:
- Workspaces — Manage multiple packages from within a singular top-level, root package. This is a huge and long-awaited feature in the community. I'm glad to see NPM releasing this and curious to see how this will influence the NPM vs Yarn debate
- Peer dependencies are installed by default — Previously NPM would print a warning if it found missing peer dependencies. You had to manually install a peer dependency to resolve the issue. NPM is now smarter about this and automatically installs peer dependencies for you
- Package-lock.json v2 and support for yarn.lock — The new package-lock format is promising to finally deliver deterministically reproducible builds. NPM will now also use the yarn.lock file if present to aid in building the dependency tree
You can read the official announcement of NPM 7 for the complete story behind this new major upgrade.
Should you upgrade?
Node.js has a release schedule that distinguishes between even and odd-numbered releases. Even-numbered releases (10, 12, 14, etc.) go into Long Term Support (LTS) whereas odd-numbered releases are short-lived. When a release reaches end-of-life, it won't get critical bug fixes or security updates anymore.
Node.js 15 reaches end-of-life on June 1, 2021. In contrast, Node.js 14 will receive security updates and critical bug fixes until April 30, 2023.
It's recommended to use even-numbered releases for production applications and odd-numbered releases for new and experimental projects.
Having said that, the Node.js team encourages you to give Node.js 15 a try and test it with your modules and applications. Node.js 16 will largely be based off Node.js 15 and the feedback it gets from the community.