Upgrading & What’s Changed
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Node.js 26 (26.10.0, released 2026-09-21, the Current line that becomes LTS on 2026-10-28), with the differences of Node.js 24 "Krypton" (24.21.0, Active LTS until 2026-10-20, then Maintenance) and Node.js 22 "Jod" (22.23.3, Maintenance) called out; versions, dates and stability levels were checked against the official documentation on 2026-10-04, and every example below was run on Node.js 26, 24 and 22 (the outputs say which line printed what; the codemod commands were run once, on 26, because they only rewrite source files). It is the page to open when a project written for an older tutorial, or for the two books named in the bibliography, has to run on a current line: it explains how to plan an upgrade, how to make Node.js tell you what will break, which deprecations are documentation-only and which already print warnings or throw, what each major from 22 to 26 changed, and where the reference books (Samer Buna, Efficient Node.js, and Jonathan Wexler, Node.js Projects, both O’Reilly, 2025) no longer match the platform. Release dates and the support policy itself live in Release Lines & LTS; this page is about what changes in your code when you cross a line.
The version baseline
| Line | Latest patch checked | Bundled npm / V8 | Phase on 2026-10-04 | Key dates |
|---|---|---|---|---|
Node.js 26 |
26.10.0 (2026-09-21) |
npm 11.19.1 / V8 14.6 |
Current |
Released 2026-05-05; LTS 2026-10-28; Maintenance 2027-10-20; end-of-life 2029-04-30. No codename assigned yet. |
Node.js 25 |
25.9.0 (2026-03-31) |
npm 11.12.1 |
End-of-life |
Released 2025-10-15; end-of-life 2026-06-01. |
Node.js 24 "Krypton" |
24.21.0 (2026-09-07) |
npm 11.19.0 / V8 13.6 |
Active LTS |
Released 2025-05-06; LTS 2025-10-28; Maintenance 2026-10-20; end-of-life 2028-04-30. |
Node.js 23 |
(no longer shipped) |
- |
End-of-life |
Released 2024-10-16; end-of-life 2025-06-01. |
Node.js 22 "Jod" |
22.23.3 (2026-09-23) |
npm 10.9.9 / V8 12.4 |
Maintenance |
Released 2024-04-24; LTS 2024-10-29; Maintenance 2025-10-21; end-of-life 2027-04-30. |
Node.js 20 "Iron" |
20.20.2 (2026-03-24) |
npm 10.8.2 |
End-of-life |
End-of-life 2026-04-30. |
Source: nodejs/Release: schedule.json
Two facts shape every upgrade plan. First, from Node.js 27 the project moves to one major release per year, every line becomes LTS, and an alpha
channel opens in October for early testing (the announcement is
Evolving the Node.js Release Schedule); the "even numbers are
production" rule ends with Node.js 26. Second, npm 12 exists (npm latest was 12.2.0 on 2026-10-03) but no Node.js line bundles it: Node.js 26.10.0
ships npm 11.19.1, so the npm version is an upgrade you control separately, covered in
npm & Package Management.
Upgrading across majors
A Node.js major is the only release type that is allowed to break documented behaviour; minor and patch releases of a line keep the stability index promises (see the stability index in Globals & Web Platform APIs). Breaking changes are announced in three places, and the upgrade workflow below uses all of them.
| Source | What it tells you | How to use it |
|---|---|---|
Release blog post of the |
The notable changes, the deprecations and removals, and every commit labelled |
Read it for each major you skip, in order: the posts for 22.0.0 through 26.0.0 are the "per-major highlights" below. |
Migration guides (blog category "migrations") |
A curated list of platform changes (supported platforms, OpenSSL, behaviour changes) plus matching codemods. Guides exist for v20 to v22 and v22 to v24; none existed yet for v24 to v26 on 2026-10-04. |
Use them as the checklist, then fall back to the release posts for the newer lines. |
The deprecations page of the API docs |
Each DEP code with its type, the version where it changed type, and the replacement. |
Search for the code printed in a warning; see the table in this page for the codes that matter most. |
The workflow that keeps surprises small is the same for a service and for a library:
-
Move one LTS to the next LTS at a time (22 to 24, then 24 to 26) while the service still has tests, and read the release post of every major in between, including the odd one that was skipped: Node.js 23 and 25 ended their lives as Current lines, but their
SEMVER-MAJORchanges are part of the jump from 22 to 24 and from 24 to 26. -
Declare the supported range in
package.jsonenginesand run the test suite on every line you promise to support, locally with a version manager and in CI with a matrix; add the next Current line (or, from 27, the alpha channel) as an allowed-to-fail job so a breaking change shows up months before it reaches LTS. -
Make deprecations visible and fatal in the test run (the next section), upgrade dependencies that print them, and run codemods for the rest.
-
Rebuild native add-ons: an add-on compiled against V8 or OpenSSL internals needs a rebuild for each major, while Node-API add-ons keep the same ABI across majors (see Native Add-ons, WebAssembly & FFI).
-
Update the base image, the
.nvmrcor.node-versionfile, and theenginesfield last, once the suite is green, and check the new line’s bundled npm (npm 10.9.9 on 22, 11.19.x on 24 and 26) against the lockfile version your team expects.
The matrix part of that workflow is a plain loop when a version manager is installed. This script runs a project’s strict test script on each line
and reports the exit code; the project is a tiny Bookshelf check with one test that calls the legacy url.parse().
#!/usr/bin/env bash
source ~/.nvm/nvm.sh
for v in 22 24 26; do
nvm use "$v" >/dev/null
npm run --silent test:strict >/dev/null 2>&1
status=$?
echo "$(node -v): npm run test:strict exits with $status"
done
{
"name": "upgrade-check",
"type": "module",
"engines": { "node": ">=22.12" },
"scripts": {
"test": "node --test",
"test:strict": "node --throw-deprecation --test"
}
}
v22.23.3: npm run test:strict exits with 0
v24.21.0: npm run test:strict exits with 1
v26.10.0: npm run test:strict exits with 1
Source: Node.js CLI: --throw-deprecation
The same test passes with plain npm test on all three lines (observed), and test:strict is the only script that fails on 24 and 26. The reason is
in the next section: url.parse() is a documentation-only deprecation on 22 and a warning-printing one on 24 and 26.
Making Node.js show what will break
Node.js has four kinds of deprecation, and they differ in what you see:
| Type | Behaviour | How to surface it |
|---|---|---|
Documentation-only |
Appears only in the API docs and generates nothing at run time. |
Some are marked as supporting |
Application (non- |
A process warning printed once, the first time the API is used from code that is not under |
With |
Runtime (all code) |
A process warning printed once for any caller, including dependencies. |
|
End-of-Life |
The API was removed or now throws; the code needs a replacement before the upgrade. |
Look for |
Source: Node.js API: Deprecated APIs
The flags that control all of this are Stable command-line options: --trace-deprecation, --throw-deprecation, --pending-deprecation and
--no-deprecation (and their NODE_OPTIONS forms), and --disable-warning=code-or-type, which became Stable in v26.7.0 and v24.20.0 (it was added
in v21.3.0). The script below calls two APIs that are deprecated differently, and prints the stack of each warning with --trace-deprecation:
import { isArray } from 'node:util'; // DEP0044, runtime since v22.0.0
import { parse } from 'node:url'; // DEP0169, application since v24.0.0
console.log('isArray:', isArray([]));
console.log('host:', parse('https://example.com:8080/a?b=1').host);
isArray: true
host: example.com:8080
(node:88840) [DEP0044] DeprecationWarning: The `util.isArray` API is deprecated. Please use `Array.isArray()` instead.
(Use `node --trace-deprecation ...` to show where the warning was created)
isArray: true
host: example.com:8080
(node:89139) [DEP0044] DeprecationWarning: The `util.isArray` API is deprecated. Please use `Array.isArray()` instead.
(Use `node --trace-deprecation ...` to show where the warning was created)
(node:89139) [DEP0169] DeprecationWarning: `url.parse()` behavior is not standardized and prone to errors that have security implications. Use the WHATWG URL API instead. CVEs are not issued for `url.parse()` vulnerabilities.
(node:90213) [DEP0044] DeprecationWarning: The `util.isArray` API is deprecated. Please use `Array.isArray()` instead.
at file:///…/dep-demo.mjs:4:25
at ModuleJob.run (node:internal/modules/esm/module_job:569:25)
…
(node:90213) [DEP0169] DeprecationWarning: `url.parse()` behavior is not standardized and prone to errors that have security implications. …
at urlParse (node:url:137:13)
at file:///…/dep-demo.mjs:5:22
…
With --throw-deprecation the first warning is thrown as an exception whose code is DEP0044, so a CI job fails at the offending line; with
--disable-warning=DEP0044 --disable-warning=DEP0169 or --no-deprecation the run is silent (all four variants were run on 26.10.0). Silencing is a
tool for a dependency you cannot change yet, not a way to finish an upgrade.
Process warnings are also delivered to the 'warning' event on process, which makes a deprecation audit a small script that works even when the output is
silenced (the --no-warnings flag below only hides the printing, not the event):
import { createHash } from 'node:crypto';
import { parse } from 'node:url';
const codes = new Set();
process.on('warning', (warning) => {
if (warning.name === 'DeprecationWarning') codes.add(warning.code);
});
createHash('shake128').update('bookshelf').digest('hex'); // DEP0198
parse('https://example.com/books?id=1'); // DEP0169
new Buffer(4); // DEP0005
process.on('exit', () => {
console.log([...codes].sort().join(' ') || 'no deprecations');
process.exitCode = codes.size > 0 ? 1 : 0;
});
| Node.js | Default run | With --pending-deprecation |
|---|---|---|
22.23.3 |
|
|
24.21.0 |
|
|
26.10.0 |
|
|
Source: Node.js API: DEP0198
Read that table as a forecast: the code that is silent on the line you run today is the code that warns on the next one, so running the suite
with --pending-deprecation on Node.js 22 already shows the deprecations that 24 and 26 will print. (new Buffer() is an application
deprecation that has been printing for years; the SHAKE example became a runtime deprecation in v25.0.0.)
Userland Migrations and codemods
Node.js maintains official codemods for "userland" code, the code outside the node executable. They are published under the @nodejs scope of the
Codemod registry, are reviewed or written by Node.js members, and cover the end-of-life deprecations and breaking changes that have a mechanical
fix. The Learn page recommends running them on a separate branch, reviewing the diff, running the tests, and formatting or linting afterwards.
Migration guides list per-major codemods; for example, the guides name @nodejs/import-assertions-to-attributes for v20 to v22, and
@nodejs/crypto-rsa-pss-update, @nodejs/dirent-path-to-parent-path, @nodejs/fs-access-mode-constants, @nodejs/fs-truncate-fd-deprecation,
@nodejs/http2-priority-signaling, @nodejs/process-assert-to-node-assert and @nodejs/tls-create-secure-pair-to-tls-socket for v22 to v24.
The deprecations page also prints the command next to the DEP codes that have one (for example DEP0044 and DEP0030). The Learn section additionally
ships guides that replace third-party packages with built-ins: chalk, kleur and ansi-colors to util.styleText(), axios to the WHATWG
fetch, and mocha to the Node.js test runner, plus one that corrects TypeScript import specifiers.
The Bookshelf scratch project had a file that still used the removed import-assertion syntax and one that used util.isArray():
// old.mjs
import data from './data.json' assert { type: 'json' };
console.log(data.name);
// legacy2.mjs
import { isArray } from 'node:util';
console.log(isArray([1, 2]));
npx codemod@latest run @nodejs/import-assertions-to-attributes --no-interactive --disable-analytics
npx codemod@latest run @nodejs/util-is --no-interactive --disable-analytics
// old.mjs
import data from './data.json' with { type: 'json' };
console.log(data.name);
// legacy2.mjs
console.log(Array.isArray([1, 2]));
Three observations from running them, labelled as observed behaviour of that day’s CLI and registry packages and not as a promise. The Learn page
shows npx codemod <codemod-name>; the CLI version used here needed the run subcommand, and without --no-interactive it stopped with "The
input device is not a TTY". It refuses to run on a dirty git tree unless you add --allow-dirty, and --dry-run reports "Would modify 1 file"
without writing. Finally, when a file had two separate node:util import lines (one for isArray, one for _extend), both the util-is and the
util-extend-to-object-assign codemods removed both imports and left the other identifier unresolved. That is why the Learn page insists on
reviewing every diff and running the tests after a codemod.
The deprecations that matter
The full list has more than two hundred entries, most of them old end-of-life ones. The table below keeps the codes that an application written for Node.js 18 to 22 is most likely to meet when moving to 24 and 26, in the state recorded by the 26.x API documentation. "Since" gives the version where the entry reached its current type; the history column shows the earlier steps.
| Code | API | Current type and since | Earlier steps and replacement |
|---|---|---|---|
DEP0005 |
|
Application, since v10.0.0 |
Use |
DEP0030 |
|
End-of-Life, v25.0.0 |
Runtime in v24.0.0 (prints on 24). Use |
DEP0040 |
|
Application, v23.7.0 / v22.14.0 |
Runtime in v21.0.0, then relaxed to application scope. Use a userland alternative. |
DEP0044 |
|
Runtime, v22.0.0 |
Use |
DEP0045 to DEP0059 |
|
End-of-Life, v23.0.0 |
Runtime on 22.0.0. Replace with |
DEP0060 |
|
Runtime, v22.0.0 |
Use |
DEP0063 |
|
End-of-Life, v26.0.0 |
Runtime in v25.0.0. Use |
DEP0147 |
|
End-of-Life, v25.0.0 |
Use |
DEP0169 |
|
Application, v24.0.0 |
Documentation-only since v19.9.0 / v18.17.0, with |
DEP0176 |
|
End-of-Life, v25.0.0 |
Runtime in v24.0.0. Use |
DEP0182 |
Short GCM authentication tags without an explicit |
End-of-Life, v26.0.0 |
Runtime in v23.0.0. Pass |
DEP0184 |
|
Runtime, v24.0.0 |
Documentation-only in v22.9.0. Use |
DEP0185 |
|
End-of-Life, v25.0.0 |
Runtime in v24.0.0. |
DEP0187 |
Invalid argument types to |
Runtime, v24.0.0 |
Documentation-only in v23.4.0 / v22.13.0; it will throw in a future version. |
DEP0190 |
An |
Runtime, v24.0.0 |
Documentation-only in v23.11.0 / v22.15.0. Arguments are only space-joined, so they are not escaped; drop the shell and keep the array. |
DEP0193 |
|
End-of-Life, v26.0.0 |
Runtime in v24.2.0. Use |
DEP0195 |
|
Documentation-only, v24.2.0 / v22.17.0 |
Codemod: |
DEP0197 |
|
Documentation-only, v24.2.0 |
Use |
DEP0198 |
SHAKE-128 and SHAKE-256 without |
Runtime, v25.0.0 |
Documentation-only with |
DEP0201 |
|
Runtime, v26.0.0 |
Documentation-only in v25.7.0. Use |
DEP0203 and DEP0204 |
Passing a |
Runtime, v26.0.0 |
Documentation-only in v25.9.0; both will throw in a future version. |
DEP0205 |
|
Runtime, v26.0.0 |
Documentation-only in v25.9.0. Use |
DEP0206 |
|
Documentation-only, v26.2.0 |
Today it returns an empty buffer; it will throw in a future version. |
DEP0209 |
Using |
Documentation-only, v26.8.2 |
Prefer |
Source: Node.js API: Deprecated APIs
Two of those entries have a nuance that older summaries get wrong. DEP0169 is an application deprecation, not a runtime one (a blog post for 24.0.0
calls it a runtime deprecation, but the API documentation, which is authoritative, lists the type as application); that is why a dependency in
node_modules can call url.parse() silently unless --pending-deprecation is set. And DEP0190 has been a runtime deprecation since v24.0.0,
which is the first time the warning is visible without a flag. Two entries were revoked and are safe to keep using: DEP0033, EventEmitter.listenerCount()
(revoked in v25.4.0), and DEP0163, channel.subscribe(onMessage) (revoked in v24.8.0); DEP0116, the legacy URL API as a whole, was revoked earlier
and its remaining parts are covered by DEP0169.
The script that follows checks, on each line, which of the removed members are still there. It was run with --no-warnings so that only the table is printed,
which is why Node.js 22 and 24 show them as present although most of them print a warning there.
import buffer from 'node:buffer';
import fs from 'node:fs';
import http from 'node:http';
import { mkdtempSync, mkdirSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
const results = {};
results['buffer.SlowBuffer'] = typeof buffer.SlowBuffer;
results['fs.F_OK'] = typeof fs.F_OK;
results['ServerResponse#writeHeader'] = typeof http.ServerResponse.prototype.writeHeader;
results['fs.constants.F_OK'] = typeof fs.constants.F_OK;
const dir = mkdtempSync(join(tmpdir(), 'rm-'));
mkdirSync(join(dir, 'a', 'b'), { recursive: true });
try {
fs.rmdirSync(join(dir, 'a'), { recursive: true });
results['rmdirSync recursive'] = 'removed the tree';
} catch (err) {
results['rmdirSync recursive'] = err.code;
}
fs.rmSync(dir, { recursive: true, force: true });
for (const name of ['node:_stream_readable', 'node:_stream_wrap']) {
try { await import(name); results[name] = 'loads'; }
catch (err) { results[name] = err.code; }
}
console.table(results);
| Probe | Node.js 22.23.3 | Node.js 24.21.0 | Node.js 26.10.0 |
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
removed the tree |
removed the tree |
throws |
|
load |
load |
|
Source: Node.js API: DEP0030; Node.js API: DEP0147
A second probe confirms the Node.js 23 removals: on 22.23.3 util.isBoolean, util.log and process.assert are still functions, and on 24.21.0 and 26.10.0
they are undefined, while util.isArray is still a function on all three; crypto.createCipher (removed in 22.0.0, DEP0106) is undefined on all three.
The same migration pattern applies to each: search the code base for the removed name, apply the replacement in the table, then run the suite
on the new line.
Worked replacements
Four changes cause most upgrade failures in real projects: customization hooks, import syntax, TypeScript syntax and shell-based child processes.
module.register() becomes module.registerHooks()
module.register() registered asynchronous hooks that ran in a separate thread; it is Deprecated (Stability 0), became documentation-only deprecated in
v25.9.0 and warns at run time since v26.0.0. module.registerHooks() is synchronous, runs in the same thread, works for every module type, and
is Stability 1.2 (Release candidate) since v25.4.0 and v24.13.1; it was added in v23.5.0 and v22.15.0.
// hooks-old.mjs
import { register } from 'node:module';
register('./loader-hooks.mjs', import.meta.url);
console.log('registered with module.register()');
// hooks-new.mjs
import { registerHooks } from 'node:module';
registerHooks({
resolve(specifier, context, nextResolve) {
return nextResolve(specifier, context);
},
});
console.log('registered with module.registerHooks()');
registered with module.register()
(node:97226) [DEP0205] DeprecationWarning: `module.register()` is deprecated. Use `module.registerHooks()` instead.
registered with module.registerHooks()
The warning appeared only on 26; both forms ran silently on 24.21.0 and 22.23.3. The details of writing hooks are in ESM & CommonJS Interoperability.
Import assertions become import attributes
The assert { type: 'json' } form was standardised as with { type: 'json' }; Node.js dropped support for assertions in 22.0.0 (it had accepted
attributes since v21.0.0, v20.10.0 and v18.20.0). Every line from 22 to 26 reports a SyntaxError for the old keyword, and the codemod shown above
does the rewrite.
// assert.mjs: SyntaxError: Unexpected identifier 'assert' on 22, 24 and 26
import data from './data.json' assert { type: 'json' };
// with.mjs: prints "bookshelf" on 22, 24 and 26
import data from './data.json' with { type: 'json' };
console.log(data.name);
Source: Node.js API: Import attributes
Enums and other non-erasable TypeScript syntax
Type stripping runs .ts files directly by erasing types; it was added in v22.6.0, is on by default since v23.6.0 and v22.18.0, prints no
experimental warning since v24.3.0 and v22.18.0, and is Stable since v25.2.0 and v24.12.0. The flag that used to transform enums, namespaces and
parameter properties, --experimental-transform-types, was removed in v26.0.0. On 26 there is no flag to fall back on, so the code must use
erasable syntax. Run on each line with the default settings, an enum file fails with ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX ("TypeScript enum is not supported in
strip-only mode"), and with --experimental-transform-types it ran on 22.23.3 and 24.21.0 (with an ExperimentalWarning) but is "bad option" on 26.10.0.
// ts.ts: fails in strip-only mode on 22, 24 and 26
enum Color { Red, Green }
console.log(Color.Green);
// ts-erasable.ts: prints 1 on 22.23.3, 24.21.0 and 26.10.0
const Color = { Red: 0, Green: 1 } as const;
type Color = (typeof Color)[keyof typeof Color];
const c: Color = Color.Green;
console.log(c);
Source: Node.js API: Type stripping
More on what strip-only mode accepts, and the tsconfig options that enforce it, is in
TypeScript in Node.js.
An args array with a shell
Passing an arguments array together with shell: true does not escape the arguments; they are only joined with spaces, which is a shell-injection hazard
(DEP0190). It prints a runtime warning from v24.0.0. The safe form is to keep the array and drop the shell.
import { execFileSync } from 'node:child_process';
// Deprecated since 24.0.0 (DEP0190): an args array together with shell: true
console.log(execFileSync('echo', ['hello', 'world'], { shell: true, encoding: 'utf8' }).trim());
// Replacement: no shell, arguments stay separate array items
console.log(execFileSync('echo', ['hello', 'world'], { encoding: 'utf8' }).trim());
Both lines printed hello world on all three lines, and [DEP0190] DeprecationWarning: Passing args to a child process with shell option true can lead to security
vulnerabilities, as the arguments are not escaped, only concatenated. appeared only on 24.21.0 and 26.10.0.
Source: Node.js API: DEP0190
On Windows, .bat and .cmd files cannot be launched with execFile() because they need a terminal; the child-process documentation lists exec(),
or spawning cmd.exe explicitly, as the ways to run them, and marks spawn() with the shell option as not recommended because of DEP0190. See
Child Processes for the argument-array rules.
Highlights per major
Each table lists what changed in the release, in the order an upgrade is most likely to hit it. Version numbers for follow-up changes come from the history entries of the API documentation, and all release dates come from the release posts.
Node.js 22 (released 2024-04-24, LTS 2024-10-29)
| Area | Change | Version |
|---|---|---|
Engine |
V8 12.4, Maglev compiler enabled on supported architectures. |
22.0.0 |
Modules |
|
22.0.0, 22.12.0 |
Modules |
Import assertions ( |
22.0.0 |
Modules |
Syntax detection enabled by default: an ambiguous |
22.7.0 |
Hooks |
|
22.15.0 |
CLI |
|
22.0.0 |
Web platform |
The |
22.0.0 |
Streams |
The default |
22.0.0 |
File system |
|
22.0.0, 22.17.0 |
Data |
|
22.5.0, 22.13.0 |
TypeScript |
Type stripping added behind a flag ( |
22.6.0, 22.18.0 |
Configuration |
|
22.21.0 |
Node.js 23 (released 2024-10-16, end-of-life 2025-06-01)
| Area | Change | Version |
|---|---|---|
Modules |
|
23.0.0, 23.5.0 |
Platform |
Support for 32-bit Windows removed. |
23.0.0 |
CLI |
|
23.0.0 |
Removals |
|
23.0.0 |
Data |
|
23.4.0 |
Hooks |
|
23.5.0 |
TypeScript |
Type stripping enabled by default. |
23.6.0 |
Deprecation |
|
23.7.0 |
Node.js 24 "Krypton" (released 2025-05-06, LTS 2025-10-28)
| Area | Change | Version |
|---|---|---|
Engine |
V8 13.6: |
24.0.0 |
npm |
npm 11 bundled (24.21.0 ships npm 11.19.0). |
24.0.0 |
Context |
|
24.0.0 |
Web platform |
|
24.0.0 |
Security |
The permission-model flag is |
24.0.0 |
Testing |
The test runner waits for subtests automatically, with no need to |
24.0.0 |
Networking |
Undici 7. |
24.0.0 |
Crypto |
OpenSSL 3.5 with security level 2: RSA, DSA and DH keys shorter than 2048 bits, ECC keys shorter than 224 bits and RC4 cipher suites are prohibited. |
24.0.0, 24.7.0 |
Platform |
No pre-built binaries for 32-bit ARMv7 Linux; macOS binaries require macOS 13.5 or later; building from source needs GCC 12.2 on Linux or AIX and Xcode 16.1 on macOS. |
24.0.0 |
Deprecations |
Runtime: |
24.0.0 |
Modules |
|
24.2.0 |
TypeScript |
Type stripping Stable. |
24.12.0, 24.15.0 |
Node.js 25 (released 2025-10-15, end-of-life 2026-06-01)
| Area | Change | Version |
|---|---|---|
Engine |
V8 14.1: faster |
25.0.0 |
Security |
|
25.0.0 |
Web platform |
Web Storage unflagged; |
25.0.0 |
Distribution |
Corepack is no longer distributed with Node.js; install it from npm if you need it. |
25.0.0 |
Removals |
|
25.0.0 |
Compile cache |
A portable compile cache and WebAssembly JSPI. |
25.0.0 |
Modules |
|
25.4.0 |
Single executables |
|
25.5.0 |
Data |
|
25.7.0 |
TypeScript |
Type stripping Stable (also v24.12.0). |
25.2.0 |
Node.js 26 (released 2026-05-05, LTS 2026-10-28)
| Area | Change | Version |
|---|---|---|
Language |
The Temporal API is enabled by default ( |
26.0.0 |
Networking |
Undici 8. |
26.0.0 |
Removals |
|
26.0.0 |
Runtime deprecations |
|
26.0.0 |
Buffers |
|
26.3.0 |
CLI |
|
26.7.0, 26.9.0 |
Deprecations |
DEP0206 ( |
26.2.0, 26.8.2 |
Networking |
The default |
observed |
Detecting the line at run time
Feature detection beats version sniffing when a library has to run on several lines, and a short probe also documents what your own upgrade depends on. This script checks the Release-candidate, Stable and Experimental APIs that differ across 22, 24 and 26:
import { createRequire } from 'node:module';
import * as sqlite from 'node:sqlite';
const require = createRequire(import.meta.url);
const crypto = require('node:crypto');
const checks = {
'require(esm) enabled': process.features.require_module,
'Temporal': typeof globalThis.Temporal,
'URLPattern': typeof globalThis.URLPattern,
'Error.isError': typeof Error.isError,
'node:sqlite DatabaseSync': typeof sqlite.DatabaseSync,
'crypto.argon2Sync': typeof crypto.argon2Sync,
'Uint8Array.fromBase64': typeof Uint8Array.fromBase64,
'module.registerHooks': typeof require('node:module').registerHooks,
'fs.glob': typeof require('node:fs').glob,
'localStorage (no flag)': Object.hasOwn(globalThis, 'localStorage') ? 'present' : 'absent',
};
console.log(process.version, 'V8', process.versions.v8.split('.').slice(0, 2).join('.'));
console.table(checks);
| Check | 22.23.3 (V8 12.4) | 24.21.0 (V8 13.6) | 26.10.0 (V8 14.6) |
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
absent |
absent |
present |
On 26 the localStorage property exists, but reading it without --localstorage-file=<file> prints an ExperimentalWarning saying that localStorage is not
available because the flag was not provided (observed on 26.10.0); the flag is Stability 1.2, Release candidate. A similar probe of language features gave
this result: RegExp.escape, Float16Array and Promise.try are functions on 24 and 26 and undefined on 22; Iterator.concat(), Map.prototype.getOrInsert() and
Uint8Array.prototype.toBase64() are functions only on 26; Set.prototype.union and Symbol.dispose exist on all three lines; and a using declaration is a
SyntaxError on 22.23.3.
Version-dependent behaviour to know about
The collected notes below were verified against the official pages for the stability level and "added in" version, and measured on the three lines where a behaviour is better shown than described. Items marked "observed" describe what a given patch release did and are not a promise.
| Topic | State on 2026-10-04 | Where it differs |
|---|---|---|
|
Stability 3, Legacy: "use `queueMicrotask()`". |
The Legacy marking dates from v22.7.0 and v20.18.0, so it applies on 22, 24 and 26; it was not a 24 or 25 change. |
Permission model |
Stable since 20.0.0 for the filesystem, child-process and worker scopes; flag renamed to |
|
|
Stability 1.2, Release candidate since v25.7.0. |
Added in v22.5.0 behind a flag, unflagged in v23.4.0 and v22.13.0. |
|
Stability 2, Stable; no flag since v22.12.0 / v23.0.0 / v20.19.0. |
"No longer experimental" in v25.4.0 and v24.15.0; the 22.x documentation still marks it Release candidate (Stability 1.2). |
Type stripping |
Stable on 24 (v24.12.0) and 26 (v25.2.0). |
On 22.23.3 it is on by default but the documentation has no stable level; |
|
Removed in 25.0.0. |
Still exported on 24, where it prints DEP0030 (observed on 24.21.0). |
|
Stability 1, Experimental; global since v24.0.0. |
Absent on 22. On 24.21.0 and 26.10.0 it ran without an |
Web Storage |
Stability 1.2, Release candidate; enabled by default since v25.0.0. |
On 26 it needs |
Config file |
|
The 22.x documentation marks it Stability 1.0, Early development; keep it out of projects that must run on 22. |
Single executable applications |
Stability 1.1, Active development. |
|
Environment files |
|
From v24.10.0 and v22.21.0. |
|
Stable since v23.5.0 and v22.13.0. |
Hex colours are documented as added in 26.1.0, but a hex colour already worked on 24.21.0 (observed). |
Source: Node.js API: process.nextTick(); Node.js API: Permissions; Node.js API: SQLite; Node.js API: Loading ECMAScript modules using require()
The platform keeps adding experimental surface while it stabilises older features, which is the other half of upgrade planning: on Node.js 26,
node:ffi (v26.1.0), the --allow-fs-vfs virtual file system, the iterable-streams module stream/iter (flag-gated), the Worker web-worker global
(--experimental-web-worker, rejected on 22 and 24) and mock.module() in the test runner are Experimental or in early development, so keep them out of
code that has to run on a Maintenance line. Pages in this section mark the stability level of every API they use; start from the stability index
described in Globals & Web Platform APIs.
What changed recently
| Date or version | Change |
|---|---|
2025-10-15 |
Node.js 25.0.0: Corepack no longer distributed, |
2025-12-09 |
npm classic tokens permanently revoked (GitHub changelog); publishing uses granular tokens or trusted publishing (OIDC). |
2026-03-10 |
The new release schedule is announced: one major per year from Node.js 27, every line becomes LTS, alpha channel from October. |
2026-04-30 |
Node.js 20 reaches end-of-life. |
2026-05-05 |
Node.js 26.0.0: Temporal, V8 14.6, Undici 8, |
2026-06-01 |
Node.js 25 reaches end-of-life. |
2026-07-08 |
npm 12.0.0: dependency install scripts blocked by default, |
2026-10-20 |
Node.js 24 moves to Maintenance. |
2026-10-28 |
Node.js 26 becomes Active LTS; the Node.js 27 alpha channel opens. |
2027-04-22 |
Node.js 27.0.0 is scheduled, the first line of the annual model. |
2027-04-30 |
Node.js 22 reaches end-of-life. |
The practical consequence is a tight window: with Node.js 22 ending on 2027-04-30 and Node.js 24 moving to Maintenance on 2026-10-20, a service on 22 should plan the move to 24 or 26 now, and a service on 24 should plan the move to 26 during the first half of 2027.
Where the books are outdated
Both reference books were published in 2025 and were written against Node.js 20 to 22 era behaviour. Everything below was verified against the official documentation on 2026-10-03 and re-checked where this page says so. The statements are summarised in the author’s own words, with the current approach next to them, and the same notes appear on the page of the concept they affect. The books are Efficient Node.js by Samer Buna and Node.js Projects by Jonathan Wexler, both published by O’Reilly Media.
| Area | What the older material assumes | Current approach |
|---|---|---|
Version baseline |
Node.js 18 or 20 as the minimum, and Docker images based on |
Node.js 18 and 20 are end-of-life. Use a |
Corepack |
Enabling Yarn or pnpm through Corepack as part of the installation. |
Corepack is not distributed since Node.js 25.0.0; install it from npm or use the package manager directly (see Installing Node.js). |
Restarting on change |
|
|
Script runners |
|
|
Environment files |
The |
|
Prompts and colours |
|
|
Local database |
The |
|
Password hashing |
|
|
HTTP and WebSocket clients |
|
Global |
Globbing |
|
|
Test libraries |
A test framework for snapshots and module mocks. |
The |
| Area | What the older material assumes | Current approach |
|---|---|---|
Module detection |
A |
Syntax detection is on by default since v22.7.0 for ambiguous files; |
|
ES modules cannot be loaded from CommonJS. |
|
Import assertions |
|
|
Customization hooks |
|
|
|
|
|
TypeScript |
A compile step with |
Node runs erasable-syntax |
Permission model |
Mentioned only as an upcoming feature. |
Stable for filesystem, child-process and worker scopes ( |
| Area | What the older material assumes | Current approach |
|---|---|---|
Production installs |
|
|
Publishing credentials |
Classic npm tokens. |
Classic tokens were revoked on 2025-12-09; use granular tokens or trusted publishing with provenance (trusted publishers). |
npm 12 |
Install scripts always run. |
npm 12.0.0 blocks dependency scripts unless allowed with |
Shell and arguments |
|
Runtime-deprecated since v24.0.0 (DEP0190); keep the array and drop the shell. Windows |
Microtasks |
|
Legacy since v22.7.0; prefer |
URLs |
|
The WHATWG |
Removed members |
|
Removed in v25.0.0 and v26.0.0; use |
Release model |
Even-numbered lines are the production lines. |
From Node.js 27 every line is LTS and there is one major per year. |
| Topic | Current state |
|---|---|
Express and Fastify |
Express 5 is the current major (npm |
create-react-app |
Deprecated by the React team (react.dev announcement). |
Job queues |
The Bull repository points to BullMQ for the next major version of the library; BullMQ is the maintained choice for new projects. |
Docker Compose |
The |
Gemini SDK and model |
The |
MongoDB server |
MongoDB 5.0 reached end-of-life on 2024-10-31 and 6.0 on 2025-07-31; run a supported release (7.0, 8.0 or later) for new work. |
Adding APT repositories |
|
Editors |
GitHub sunset the Atom editor on 2022-12-15 and archived its repositories; use a maintained editor such as VS Code. |
HTTP/2 server push |
Chrome 106 disabled server push by default and it is effectively dead on the web; send |
Source: Fastify: LTS; Gemini API: Deprecations; React: Sunsetting Create React App; Docker Docs: Compose version and name; MongoDB: Software Lifecycle Schedules; Debian: apt-key(8); Debian: sources.list(5); GitHub Blog: Sunsetting Atom; Chrome for Developers: Remove HTTP/2 Server Push from Chrome; Chrome for Developers: 103 Early Hints
Insecure practices that the projects should not copy
Some sample projects in Node.js Projects (Jonathan Wexler) are teaching code, not production code, and a reader who copies them inherits their weaknesses. The safe form for each:
| Area | Weakness in the sample projects | Safe form |
|---|---|---|
Stored secrets |
A password manager that keeps the managed passwords readable. |
Derive a key with scrypt or Argon2id and encrypt each secret with AES-256-GCM, storing the IV and auth tag with the ciphertext (see Crypto, Passwords & Secrets). |
Password hashing |
A low-iteration, synchronous PBKDF2 whose hashes are compared with |
Use at least 210,000 iterations for PBKDF2-HMAC-SHA512 (OWASP), or better Argon2id or scrypt, the asynchronous API and |
Configuration |
Hard-coded secrets and JWT secrets that fall back to a fixed string. |
Fail fast at start-up when a secret is missing, and read it from |
Tokens |
JWTs issued without an expiry and verified without pinned algorithms; session cookies without |
Set |
Request protection |
No CSRF protection, no rate limiting, and an email address placed in a verification URL. |
Add CSRF defences and rate limits, and verify email with a random single-use token. |
Scraping and templates |
A scraper that spoofs its user agent and ignores robots.txt; a vulnerable version of the |
Respect robots.txt and site terms, identify your client, and keep template engines up to date (see Email, Feeds & Web Scraping). |
Cloud database access |
A MongoDB Atlas network access list open to every address. |
Restrict access to the addresses that need it, and use credentials from the environment. |
LLM prompts |
User input concatenated into a single prompt string, and the model’s reply used as if it were trusted data. |
Prompts are injectable: keep instructions in the system role and user text in the user role, treat model output as untrusted input, and request structured output that you validate before use (see Integrating LLM APIs). |
Errors in the books worth knowing
Typos and slips are normal in technical books, and the page that covers each concept already shows the right form; the ones below are collected so that a reader following either book can check them in one place. The correct form is given, not the passage.
| Topic | Correct form |
|---|---|
Promise helper module |
The module is |
Awaiting several promises |
|
Per-request child processes |
A child forked per request must exit, or each request leaks a process; a request or response object cannot be sent to a worker (it is not serialisable). |
Client IP hashing |
An address-hashing sketch for sticky balancing must handle IPv6 as well as IPv4. |
ES modules |
Use |
|
The separator is |
|
It passes when the string does not match the pattern and throws when it does. |
|
A DNS lookup failure, not "entity not found". |
Pausing a stream |
Removing |
Backpressure |
Large generators must respect backpressure by honouring the result of |
Encryption |
|
Promise chains |
A |
|
|
Orders test fixtures |
The orders in the testing example have no ids, so the tests pass by accident; give every fixture a unique id so a lookup, update or delete can match only the intended record. |
Source: Node.js Learn: Backpressuring in Streams; Node.js API: Stream; Node.js API: Assert; Node.js API: Errors (ERR_FS_FILE_TOO_LARGE); MDN: Promise.prototype.finally(); Node.js API: Command-line API (NODE_PATH)
| Topic | Correct form |
|---|---|
|
The readline interface used in the example must be created before it is used. |
|
It needs a |
Not-found handlers |
A Fastify |
Sequelize |
|
Schedulers and queues |
Use the documented |
Blockchain project |
A transaction needs its own class, and a proof-of-work nonce must be bound to the block it signs. |
LLM errors |
An error object returned by the API is not an answer; check the status and the error field first. |
MongoDB connection strings |
|
Topics the books do not cover
Where the books say little or nothing, the official documentation supplies it, and this section has a page for each: the real libuv event-loop phases
and queueMicrotask() versus process.nextTick() versus setImmediate(), AbortSignal, AsyncLocalStorage and using (Event Loop,
Async Context & Cancellation); "exports" and "imports", conditional exports, dual packages and
require(esm) (package.json: exports & imports); Web Streams, Buffer, fs.glob() and HTTP/2
(Web Streams & Interop, HTTPS, TLS & HTTP/2); worker_threads
(Worker Threads); snapshots, module mocks and coverage (Mocking, Snapshots & Coverage);
profilers, heap snapshots and diagnostic reports (Profiling & Performance); and the official threat model, the permission
model and supply-chain hygiene (Security Best Practices, Permission Model).