What Is New in Node.js 16
| Category | Change |
|---|---|
| New Features |
|
| Improvements |
|
| Deprecations |
|
| Breaking Changes |
|
Does Node.js 16 Finally Run Natively on Apple Silicon?
Yes - Node.js 16 is the first release to ship official prebuilt binaries for Apple Silicon (darwin-arm64).
If you have been running x64 binaries through Rosetta 2, this is the upgrade you have been waiting for.
The macOS .pkg installer is a universal "fat" binary covering both Intel and ARM in a single download, so your CI scripts and install playbooks do not need to branch on architecture.
On the build side, ASLR (Address Space Layout Randomization via Position Independent Executable) is now enabled on macOS.
That is a compile-time hardening change - nothing you need to configure, but worth knowing if you do any low-level debugging.
The build system also drops Python 2 entirely; if you are building Node from source or compiling native addons in a CI pipeline, make sure python3 is on the path.
In practice, switching to the ARM binary on an M1/M2 machine tends to produce a noticeable speed-up in CPU-heavy workloads like transpilation or test running. It is one of the most immediately tangible benefits of this release for Mac-heavy development teams.
What Does V8 9.0 Actually Give You as a JavaScript Developer?
The most developer-visible addition from V8 9.0 is RegExp Match Indices, which lets you find exactly where in a string a capture group matched - not just what it matched.
You opt in with the /d flag, and then the .indices property on the result gives you [start, end] pairs for every group.
const re = /(?<year>\d{4})-(?<month>\d{2})/d;
const match = re.exec('Release: 2021-04');
console.log(match.indices.groups.year); // [9, 13]
console.log(match.indices.groups.month); // [14, 16]
This is genuinely useful for any tooling that reports error locations, highlights syntax, or maps transformed output back to source - think linters, parsers, and code formatters. Before this, you had to track offsets yourself by counting characters after the match, which was tedious and error-prone.
The V8 bump also raises NODE_MODULE_VERSION to 93, which means any native addon compiled against an older Node version will not load and must be recompiled.
If you depend on packages with prebuilt native binaries - things like bcrypt, sharp, or better-sqlite3 - check that their maintainers have published builds for Node 16 before upgrading in production.
Can You Finally Use Async/Await With Timers Without a Wrapper?
Yes - timers/promises graduates from experimental to stable in Node 16, giving you first-class Promise-based timer functions you can await directly with no extra dependencies.
| Old callback style | New stable promises API |
|---|---|
|
|
|
|
The setImmediate promise variant is also stable, so you can defer microtask-safe work in async functions without wrapping anything in a new Promise().
This is one of those ergonomic improvements that does not unlock anything you could not do before, but it removes an entire category of boilerplate from async utility code.
Watch out for one subtlety: these are named exports from 'timers/promises', not the global setTimeout.
They shadow the global in the module scope if you import them by the same name, which occasionally surprises developers who mix callback-style and promise-style timers in the same file.
What Deprecations in Node.js 16 Will Actually Break My Code?
The biggest migration pressure comes from three areas: fs.rmdir() recursive, process.binding() internals, and package resolution fallbacks.
All three are now runtime deprecations, meaning you will see a DeprecationWarning printed to stderr the moment your process hits the code path - you do not need to look for it in docs.
fs.rmdir() recursive - switch to fs.rm()
The { recursive: true } option on fs.rmdir() and fs.rmdirSync() is runtime-deprecated and the permissive variant (which silently tolerated non-existent paths) is already removed.
The replacement is fs.rm() / fs.rmSync() with { recursive: true, force: true }.
This is a one-line find-and-replace, but do it before Node 17 or 18 closes the door completely.
process.binding() - only affects native addon authors and niche tooling
Most application developers never call process.binding() directly.
If you do, or if you depend on a package that patches Node internals through it (some older instrumentation agents do), you will start seeing warnings for bindings like http_parser, url, crypto, v8, and async_wrap.
The public APIs those bindings backed are not going away, but the back-channel access through process.binding() is being closed off.
Package resolution fallbacks - clean up your package.json
Three package resolution behaviors are now deprecated: subpath folder mappings (e.g., "exports": { "./lib/": "./src/lib/" }), implicit index.js lookups when "main" points to a directory, and invalid "main" entries that Node had been silently ignoring.
If your published package relies on any of these, consumers running Node 16 will see warnings, and future releases will error hard.
The fix is to list your exports explicitly in the "exports" field.
Common Questions about Node.js 16
Do I need to recompile my native addons when upgrading to Node.js 16?
Yes - NODE_MODULE_VERSION is now 93, so any addon compiled against an older version will refuse to load and must be rebuilt against the Node 16 headers. Run npm rebuild or check that your package maintainers have published v16-compatible prebuilds before you deploy.
What replaces fs.rmdir() recursive in Node.js 16?
Use fs.rm() or fs.rmSync() with the options object { recursive: true, force: true } instead. The force flag replicates the old permissive behavior that swallowed errors for missing paths. The recursive option on fs.rmdir() will still work in Node 16 but emits a DeprecationWarning at runtime each time that code path is hit.
Is the node: prefix in require() just cosmetic, or does it do something?
It is not just cosmetic - using require('node:fs') bypasses the module cache lookup for user-land modules with the same name, making it unambiguous that you want the built-in. It also gets REPL autocompletion for free. Most teams will not feel forced to adopt it immediately, but it is a cleaner habit to build going forward, especially in ESM codebases where the node: prefix was already the convention.
What happened to btoa and atob in Node.js 16?
They are now globals, matching browser behavior. Previously you had to import them from the buffer module or use a third-party polyfill. Any code that was already importing them from buffer will continue to work, but new code can reference btoa and atob directly without any import statement.
Does Node.js 16 require me to update my CI build environment?
Possibly. If you compile Node from source or build native addons in CI, you need Python 3 on the path and GCC 8.3 or newer (on Linux) or Xcode 11 or newer (on macOS). Prebuilt binaries from nodejs.org have no extra requirements for running Node, only for compiling it or its addons.
Why does performance not need to be imported in Node.js 16?
The perf_hooks.performance object is now a global, matching the Web Performance API that browsers expose. Code written for the browser that calls performance.now() or performance.mark() will work in Node 16 without any import, which is useful for isomorphic utilities and test helpers that want to measure time across environments.