Node 26 LTS upgrade guide: what breaks coming from Node 22 or 24

Summary

Node.js 26 becomes Active LTS on October 28, 2026, and teams on Node 22 or 24 should plan to move directly to 26 because Node 22 support ends in April 2027 while 26 lasts until April 2029. The main breakage includes removed response.writeHeader(), _stream_* modules, --experimental-transform-types, and short AES-GCM tags without an explicit authTagLength; module.register() and passing CryptoKey to crypto functions are runtime-deprecated, native addons need rebuilding for NODE_MODULE_VERSION 147, and outbound HTTP code should be retested against undici 8. Skipping Node 25 means also inheriting the removal of bundled Corepack, fs.F_OK/R_OK/W_OK/X_OK constants, and SlowBuffer, plus default Web Storage globals and changed localStorage behavior when no storage file is configured. Before upgrading, run the full test suite with --throw-deprecation, since warnings can fire asynchronously after a test appears to pass, and use a text audit for known patterns, then check dependencies and native modules and apply the official codemods for fs constants and SlowBuffer. The payoff includes Temporal enabled by default and V8 14.6 features such as Map.prototype.getOrInsert(), but verify your own suite on Node 26.12.0 or the first 26.x LTS build before production.

Node.js 26 is scheduled to become the Active LTS line on October 28, 2026, and it's the last release line under the old twice-a-year model. If you run Node 24 or 22 in production, 26 is the next version worth moving to. The upgrade is mostly painless, but a handful of APIs that warned for years are now gone. If you're coming from 22, you also inherit everything Node 25 removed, and since odd-numbered releases see little production use, that's the part people miss.

The dates that matter

The official release schedule is the authoritative record, and right now it says:

Line Status on 2026-10-21 Next change End of life
Node 22 (Jod) Maintenance LTS none 2027-04-30
Node 24 (Krypton) Maintenance LTS since 2026-10-20 none 2028-04-30
Node 26 Current (26.12.0, released 2026-10-14) Active LTS on 2026-10-28 2029-04-30

Node 24 left Active LTS yesterday, so from here on it gets security and critical fixes only. Node 22 has six months left. If you're on 22, don't stop at 24. Going straight to 26 buys you an extra year of support for the same migration effort.

Node 27 changes the model. The project's Evolving the Node.js Release Schedule announcement says that starting with 27.x there will be one major release per year, every release becomes LTS, and the odd/even split goes away. Each line gets a six-month Alpha (October to March, semver-major changes allowed), six months as Current, then 30 months of LTS. The schedule file already lists a 27 alpha starting on October 28. If you only ever upgrade to LTS versions, the post's own summary is that "little changes beyond version numbering." Library authors are asked to put the Alpha builds in CI, because by the time a release hits LTS it's too late to report a bug before users see it.

What breaks moving to Node 26

These are the removals and new runtime deprecations from the Node.js 26.0.0 release notes that real application code is likely to hit. Each entry's history comes from the deprecations reference.

  • response.writeHeader() is gone (DEP0063). It was never documented as a supported API, but older code and some frameworks call it. Use writeHead().
  • require('_stream_readable') and the other _stream_* modules are removed (DEP0193). Import from node:stream instead. This one usually lives in an old dependency, not your code.
  • Decrypting AES-GCM with a tag shorter than 16 bytes now requires an explicit authTagLength (DEP0182, end-of-life in 26).
  • The --experimental-transform-types flag is removed.
  • module.register() now prints a runtime deprecation warning (DEP0205). The replacement is module.registerHooks(), whose hooks are synchronous and run in-thread. It's been available since 23.5.0 and 22.15.0, so you can migrate before you upgrade.
  • Passing a CryptoKey to node:crypto functions such as createHmac() or createCipheriv() is runtime-deprecated (DEP0203), and the docs say it "will throw an error in a future version."

Native addons need rebuilding or new prebuilt binaries: 26.0.0 bumps NODE_MODULE_VERSION to 147, so check every package with a compiled component, private ones especially. The release also moves undici, the HTTP client implementation inside Node, to 8.0.2. That's a major version of its own, so retest code that makes outbound HTTP calls.

What you inherit from Node 25 if you skip it

The Node.js 25.0.0 release notes carried a bigger cleanup than 26 did, and a team moving from 22 to 26 gets all of it at once:

  • Corepack is no longer distributed with Node. CI scripts that run corepack enable to get pnpm or Yarn will fail until you install Corepack separately or install the package manager directly.
  • fs.F_OK, fs.R_OK, fs.W_OK and fs.X_OK are removed (DEP0176). Read them from fs.constants.
  • SlowBuffer is removed (DEP0030). Use Buffer.allocUnsafeSlow(size).
  • Web Storage is enabled by default, so localStorage and sessionStorage exist as globals. Node 26 then changed localStorage to return undefined when no storage file is configured. Code that checks typeof localStorage to decide whether it's running in a browser should switch to a more specific check.

Node 24 users have already been warned about the fs constants and SlowBuffer, since both became runtime deprecations in 24.0.0. Node 22 printed nothing for either. If your logs show deprecation warnings on 24, treat every one as a hard error on 26.

Find the problems before you upgrade

The cheapest check is to run your test suite on your current version with --throw-deprecation, which turns every deprecation warning into an exception. Here's DEP0182 on Node 23.9.0, where it's a runtime deprecation that Node 26 makes fatal:

import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';

const key = randomBytes(32);
const iv = randomBytes(12);

const cipher = createCipheriv('aes-256-gcm', key, iv, { authTagLength: 8 });
const data = Buffer.concat([cipher.update('hello'), cipher.final()]);
const tag = cipher.getAuthTag(); // 8 bytes

// No authTagLength here: the 8-byte tag is accepted only with a warning
const decipher = createDecipheriv('aes-256-gcm', key, iv);
decipher.setAuthTag(tag);
console.log(Buffer.concat([decipher.update(data), decipher.final()]).toString());
$ node --throw-deprecation gcm.mjs
hello
node:internal/process/warning:176
        throw warning;
        ^

DeprecationWarning: Using AES-GCM authentication tags of less than 128 bits without specifying the authTagLength option when initializing decryption is deprecated.
    at Decipheriv.setAuthTag (node:internal/crypto/cipher:211:22)

Notice that hello printed first. The warning is emitted asynchronously, after the decrypt has already returned, so a test that only checks return values can pass and still crash the process a moment later. Run the whole suite this way, not just one file. Passing { authTagLength: 8 } to createDecipheriv() makes the same script exit cleanly.

Deprecation flags only catch code paths your tests actually run, and on Node 22 they can't catch anything Node 22 never warned about. For those, a dumb text scan is worth the two minutes. This script flags the patterns above in source files and package.json scripts:

// node26-audit.mjs - flag code that breaks or warns on Node.js 26
// usage: node node26-audit.mjs 
import { readdir, readFile } from 'node:fs/promises';
import { join, relative } from 'node:path';

const CHECKS = [
  { re: /\.writeHeader\s*\(/, msg: 'removed in 26 (DEP0063): use res.writeHead()' },
  { re: /['"](node:)?_stream_(wrap|readable|writable|duplex|transform|passthrough)['"]/,
    msg: 'removed in 26 (DEP0193): import from node:stream' },
  { re: /\bSlowBuffer\b/, msg: 'removed in 25: use Buffer.allocUnsafeSlow()' },
  { re: /\bfs\.(F_OK|R_OK|W_OK|X_OK)\b/, msg: 'removed in 25: use fs.constants.*' },
  { re: /\bregister\s*\(\s*['"][^'"]+['"]\s*,\s*(import\.meta\.url|pathToFileURL)/,
    msg: 'runtime-deprecated in 26 (DEP0205): move to module.registerHooks()' },
  { re: /--experimental-transform-types/, msg: 'flag removed in 26' },
  { re: /\bcorepack\b/, msg: 'Corepack is no longer bundled since 25: install it yourself' },
  { re: /createDecipheriv\s*\(\s*['"]aes-\d+-gcm['"]/,
    msg: 'check authTagLength: short GCM tags without it fail in 26 (DEP0182)' },
];

const EXT = /\.(c|m)?(j|t)sx?$/;
const SKIP = new Set(['node_modules', '.git', 'dist', 'build']);

async function* walk(dir) {
  for (const entry of await readdir(dir, { withFileTypes: true })) {
    if (SKIP.has(entry.name)) continue;
    const path = join(dir, entry.name);
    if (entry.isDirectory()) yield* walk(path);
    else if (EXT.test(entry.name) || entry.name === 'package.json') yield path;
  }
}

const root = process.argv[2] ?? '.';
let hits = 0;
for await (const file of walk(root)) {
  const lines = (await readFile(file, 'utf8')).split('\n');
  lines.forEach((line, i) => {
    for (const { re, msg } of CHECKS) {
      if (re.test(line)) {
        hits++;
        console.log(`${relative(root, file)}:${i + 1}  ${msg}`);
      }
    }
  });
}
console.log(`${hits} finding(s) under ${root} (audited with Node ${process.version})`);
process.exitCode = hits ? 1 : 0;

It only uses node:fs/promises and node:path, so it runs on whatever version you have now. We ran it against a small sample project with a writeHeader() call, an fs.R_OK check, a _stream_transform require, a module.register() loader and a package.json that calls corepack enable and --experimental-transform-types:

$ node node26-audit.mjs sample
loader.mjs:3  runtime-deprecated in 26 (DEP0205): move to module.registerHooks()
package.json:4  Corepack is no longer bundled since 25: install it yourself
package.json:5  flag removed in 26
server.js:3  removed in 26 (DEP0193): import from node:stream
server.js:6  removed in 25: use fs.constants.*
server.js:7  removed in 26 (DEP0063): use res.writeHead()
6 finding(s) under sample (audited with Node v23.9.0)

It exits non-zero when it finds something, so it can sit in CI until the migration is done. It deliberately skips node_modules. For dependencies, the useful signal is the deprecation warnings when your tests run, plus a quick look at whether each package's latest release lists Node 26 in its engines field. For the fs constants and SlowBuffer, the deprecations page lists official codemods (npx codemod@latest @nodejs/fs-access-mode-constants and @nodejs/slow-buffer-to-buffer-alloc-unsafe-slow) that rewrite the calls for you.

What you get for the trouble

The headline in 26.0.0 is that the Temporal date/time API is enabled by default, so you can stop pulling in a date library just to get time zones and calendar arithmetic right. V8 moves to 14.6, which adds Map.prototype.getOrInsert() and getOrInsertComputed() (plus the WeakMap versions) and Iterator.concat(). None of that is a reason to rush. The reason to move in the next few months is the support calendar: 26 is supported until April 2029, and 22 runs out in April 2027.

One caveat about how this article was checked. Both scripts ran on Node 23.9.0, and Node 26 itself wasn't run, so the Node 26 behaviour described here comes from the release notes and the deprecations reference linked above. Before you switch production, run your own suite on 26.12.0 or the first LTS-tagged 26.x release.

NODE.JS UPGRADE NODE 26 LTS DEPRECATIONS COREPACK RELEASE SCHEDULE

  RELATED

  COMMENTS

0

No comment for this article.