Node.js 16 was released last week as the new Current release line, and while it doesn’t bring any single revolutionary feature, the collection of improvements adds up to a meaningful step forward. The release includes native Apple Silicon (M1) binaries for the first time, ships V8 9.0 with several new JavaScript language features, and stabilizes the Timers Promises API that makes working with timers in async code far more ergonomic.
Node 16 will enter Long Term Support (LTS) in October 2021, making it the version most production environments will standardize on for the next couple of years. Here’s what’s worth paying attention to.
Native Apple Silicon Support#
For the growing number of developers who have switched to Apple’s M1 machines, Node.js 16 provides prebuilt binaries for the darwin-arm64 architecture. Previous versions could run on M1 through Rosetta 2 emulation, which worked but added overhead and occasional compatibility issues with native addons.
This matters more than it might seem. The M1’s performance advantage is significant — builds that took 30 seconds on an Intel MacBook Pro can finish in under 15 seconds on M1, but only if you’re running native code. Running Node through Rosetta 2 ate into that performance gain, and some native addon compilation workflows were fragile or broken entirely.
I switched my primary development machine to an M1 MacBook Pro in February, and while the Rosetta experience was surprisingly good, there were enough paper cuts with native module compilation — particularly around node-gyp and modules that depend on architecture-specific binaries — that having official arm64 builds is a welcome relief. If you’re managing a team with mixed architectures, the official support also simplifies your CI matrix.
V8 9.0 and New Language Features#
Node.js 16 ships with V8 9.0, which brings several JavaScript features that have been working through the TC39 standards process:
RegExp match indices (d flag) — When you use the new /d flag with a regular expression, the match object includes an indices property that tells you the start and end positions of each captured group. This is incredibly useful for syntax highlighting, code editors, and any application that needs to know where in the string a match occurred, not just what matched.
const match = /(hello) (world)/.exec('say hello world');
// Standard match: ['hello world', 'hello', 'world']
const matchWithIndices = /(hello) (world)/d.exec('say hello world');
// matchWithIndices.indices: [[4, 15], [4, 9], [10, 15]]
Promise.any and AggregateError — Promise.any resolves with the first fulfilled promise (as opposed to Promise.race, which resolves with the first settled promise, whether fulfilled or rejected). If all promises reject, it throws an AggregateError containing all the rejection reasons. This is useful for patterns like trying multiple API endpoints and taking the first successful response.
Logical assignment operators — &&=, ||=, and ??= combine logical operations with assignment. These have been available in recent V8 versions, but their presence in the Node 16 LTS line means they’re now safe to use in production code without transpilation.
// Before
user.name = user.name || 'Anonymous';
// After
user.name ||= 'Anonymous';
// Nullish coalescing assignment
config.timeout ??= 3000; // Only assigns if null or undefined
The Timers Promises API#
The most practically useful addition in Node 16, in my opinion, is the stabilization of the Timers Promises API (timers/promises). This module provides promise-based versions of setTimeout, setInterval, and setImmediate that work naturally with async/await.
Before this API, using timers in async code required wrapping them in promises manually:
// The old way
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
await sleep(1000);
// Node 16 way
import { setTimeout } from 'timers/promises';
await setTimeout(1000);The setInterval version is even more compelling — it returns an async iterator, which means you can use for await...of to consume ticks:
import { setInterval } from 'timers/promises';
for await (const _ of setInterval(1000)) {
console.log('tick');
// break when done
}This is a small addition, but it eliminates one of those annoying friction points that every Node developer has encountered. I’ve probably written that sleep utility function in a hundred different projects. Having it in the standard library, with proper AbortController support for cancellation, is a quality-of-life improvement that I’ll immediately start using.
Web Crypto API (Experimental)#
Node 16 also includes an experimental implementation of the Web Crypto API, accessible via globalThis.crypto and crypto.webcrypto. This is part of the broader effort to align Node.js APIs with web platform standards, making it easier to write isomorphic code that runs in both Node and the browser.
The existing Node crypto module isn’t going anywhere, but having a standards-compliant Web Crypto API means that cryptographic code written for the browser can run in Node without modification. For library authors who target both environments, this reduces the need for platform-specific code paths and bundler configurations.
npm 7 by Default#
Node 16 ships with npm 7, which is now the default package manager. If you’ve been on Node 14 with npm 6, the upgrade brings several notable changes: automatic installation of peer dependencies, workspaces support for monorepos, and the new package-lock.json format (v2). The peer dependency change in particular can cause breakage on upgrade — packages that previously installed with only warnings may now fail if peer dependencies conflict. Run npm install in your projects after upgrading and resolve any conflicts before they surprise you in CI.
My Take#
Node.js 16 is a solid, pragmatic release. It doesn’t try to reinvent the runtime — instead, it focuses on catching up with the JavaScript language, improving platform support, and polishing APIs that developers use daily. That’s exactly the right approach for a runtime that’s become critical infrastructure for a huge portion of the web.
The Apple Silicon support and V8 9.0 upgrades are expected evolutions, but the Timers Promises API is the kind of small improvement that disproportionately improves developer experience. It’s the difference between a runtime that can do async and one that’s designed for async at every level.
If you’re currently on Node 14 LTS, there’s no rush to upgrade production systems — Node 14 is supported until April 2023. But I’d recommend starting to test your applications against Node 16 now, particularly if you have native addons or rely on specific npm 6 behaviors. When Node 16 enters LTS in October, you’ll want to be ready for a smooth transition.
