Node 26: What Breaks When You Upgrade, and When the Jump Is Worth It (LTS Guide)
DEV Community

Node 26: What Breaks When You Upgrade, and When the Jump Is Worth It (LTS Guide)

Node 26 shipped on May 5, 2026 and enters LTS on October 28. It's a calm upgrade - the list of things that actually break is short, and almost all of it has been warned about for years - but it has one property no previous version had: it's the last release of the two-per-year model. Starting with Node 27, the project moves to one release per year, and every release is LTS. The odd/even distinction that spent a decade deciding which version you could run in production disappears. That makes this upgrade more than a version bump: it's the last train on the old schedule, and it's worth boarding knowing what's inside. Here's what really breaks, sorted by impact, and at the end the full calendar so you can decide when to move. First: where are you coming from? The effort depends on your starting point: - From Node 24 (the current LTS): the comfortable scenario. The incompatible changes are few and very localized; for most projects the migration is checklist work, not refactoring work. - From Node 22: still reasonable, but with more fronts - you absorb everything that changed in 24 (V8, permissions, minimum glibc) plus what's new in 26. Node 22 leaves maintenance in April 2027, so you have a real deadline. - From Node 25: odd releases were never for production, and this was the last one that will ever exist. Jump to 26 without thinking twice. The schedule change: Node 27 debuts a new model The official project announcement sums it up: | Aspect | Through Node 26 | From Node 27 on | |---|---|---| | Major releases per year | 2 | 1 | | LTS versions | Even-numbered only | All of them | | Short-lived odd versions | Yes | Gone | | Alpha channel | Didn't exist | 6 months, allows semver-major changes | | Total support per version | 36 months | 36 months (unchanged) | The concrete dates that affect you: - Node 26: Active LTS from October 28, 2026, maintenance from October 20, 2027, end of life on April 30, 2029. - Node 27: alpha channel opens October 28, 2026 (the same day 26 goes LTS), release on April 22, 2027, and it will also be LTS. - Node 24: Active LTS until October 20, 2026, when it moves to maintenance (critical and security fixes only); end of life on April 30, 2028. The exact dates come from the schedule.json in Node's release repository, the file the project itself works from; the new schedule was announced on the Node.js blog. The practical consequence: you'll no longer have to memorize which versions "count". Every release from here on is production-grade, and one release per year means fewer migrations per decade for your team. If you maintain libraries, the part that concerns you is the alpha channel: six months to test breaking changes before each release ships. High impact 1. The internal stream* modules are gone The removal with the most shrapnel, because it doesn't break your code: it breaks old dependencies. The internal modules _stream_readable , _stream_writable , _stream_duplex , _stream_transform , _stream_passthrough and _stream_wrap - deprecated since Node 12 - have been removed entirely. Any require('_stream_readable') now throws MODULE_NOT_FOUND . No package maintained in recent years does that, but ecosystems drag fossils along. To find out whether you have one: grep -rn "stream" node_modules --include="*.js" -l | head If something shows up, you have two exits: update that package to a version using the public API (node:stream ), or - if it's abandoned - replace it. The readable-stream polyfill covers most cases where the package is yours and you just need a mechanical migration. 2. Undici 8: the global fetch gets strict Node 26 ships Undici 8.0, the implementation behind the global fetch . It brings stricter header validation and changes to redirect handling. If your code builds requests with dynamically generated headers - from a database, from user input, from another API - what used to pass silently can now throw a TypeError . The pattern that shows up most in practice: // If any value carries \n or \r (e.g. a token copied with a trailing newline), // Undici 8 rejects it instead of silently sanitizing. const res = await fetch(url, { headers: { Authorization: Bearer ${token} }, }); The right fix is not catching the error: it's sanitizing the value at the edge, where it enters your system (token.trim() when you read it). Having the request blow up before it leaves is the good behavior - with Undici 7 that token with a \n would travel and fail at the server on the other side, which is far harder to debug. 3. Native addons: mandatory recompilation NODE_MODULE_VERSION moves to 147, so every compiled addon (bcrypt, sharp, better-sqlite3, canvas…) needs to be rebuilt or ship prebuilds for the new ABI. The error is unmistakable: Error: The module was compiled against a different Node.js version using NODE_MODULE_VERSION 137. npm rebuild solves it locally; in CI and Docker, rebuilding the image on the new base is enough. The detail that can actually stall you: the build requirements went up - GCC 13.2 minimum, and Python 3.9 support was dropped from the build toolchain. If you compile addons inside an old Debian/Alpine image, update the image before fighting the wrong error. Free tool: Docker Compose Validator - Validate your compose.yaml against the official Compose schema and catch key typos, wrong types and YAML errors, with the exact line. All in your browser. Medium impact 4. module.register() is runtime-deprecated The async module-hooks API - the one used by instrumentation loaders, on-the-fly transpilation and APM agents - now emits a runtime deprecation. The designated replacement is the synchronous module.registerHooks() API. This reaches you mostly through third parties: observability agents (Datadog, New Relic, OpenTelemetry) and tools like tsx live in that layer. The action item isn't rewriting anything of yours: it's updating those dependencies to the versions that already migrated before moving production to 26. 5. writeHeader() removed from the HTTP server http.Server.prototype.writeHeader() was an undocumented alias of writeHead() . It's gone. If it appears in your code, the fix is literal, letter for letter: // Before res.writeHeader(200, { 'Content-Type': 'application/json' }); // Now res.writeHead(200, { 'Content-Type': 'application/json' }); A grep -rn "writeHeader" src/ tells you in seconds whether you're affected. Frameworks (Express, Fastify, Nest) have always used the documented API; this bites hand-written HTTP code from many years ago. 6. type: "module" packages: the extensionless-CJS exception is gone In packages declared as ESM, loading a CommonJS file without an extension relied on a historical resolver exception that Node 26 removes. If a "type": "module" package of yours had a require of a path without an explicit .cjs /.js , it now fails. The fix is adding the extension - which is what ESM resolution always asked for. Low impact - localStorage with no backing file returnsundefined instead of throwing. If you probed availability with try/catch aroundlocalStorage , invert the check: it's a plainif now. - QuotaExceededError becomes aDOMException -derived interface. Only affects code comparing the error by constructor or numericcode . - Several crypto APIs advance in the deprecation cycle - one reaches end of life and others start warning at runtime. If you see no warnings with --pending-deprecation today, you're not affected. - assert accepts printf-style messages in assertion errors. It's an addition, but it changes text output: if your CI parses assert messages (bad idea, while you're here), review it. What does NOT break, even if you've read otherwise - The Temporal API doesn't break Date . Temporal arrives enabled by default in Node 26, but it's an addition: all yourDate code keeps working exactly as (badly as) before. Migrating is optional and gradual - I have a full guide to Temporal in Node 26 with the conversion table from date-fns and dayjs. - Running TypeScript directly is now stable. node app.ts works with no flags: type stripping left experimental status. The only thing that "breaks" is the--experimental-transform-types flag, which was removed precisely because the feature graduated - if you had it in an npm script, delete it and move on. - V8 14.6 only adds. Map.prototype.getOrInsert() ,Iterator.concat() and the rest of the Chromium 146 features are additions with zero migration cost. The upgrade, in practice The order that produces the fewest surprises: - Install 26 next to your current version, not over it. With fnm or Volta it's one command, and rolling back takes seconds. - npm rebuild (or deletenode_modules and reinstall) to regenerate native addons against ABI 147. - Run the suite with deprecations visible: node --pending-deprecation --trace-deprecation ./node_modules/.bin/vitest run The module.register() and crypto ones show up here, with a stack trace to the culprit. - grep for the fossils:stream ,writeHeader( . Two minutes that save you a production surprise. - Update your agents (APM, telemetry, loaders) to versions that declare Node 26 support - it's the layer that takes longest. - Review the engines field in yourpackage.json and your critical dependencies:npm ls --depth=0 plus a glance atEBADENGINE warnings during install. When should you upgrade? New project: start on 26 today. There's no reason to begin something that will live for years on 24. Production on 24: the sensible window opens on October 28, 2026, when 26 enters LTS and eight days after 24 moves to maintenance - with 24 supported until April 2028 there's no rush, but no reason to wait until the end either: the longer you wait, the further this list drifts from fresh memory. Production on 22: move now. April 2027 looks far away until it doesn't, and the 22 β†’ 26 jump is bigger than 24 β†’ 26. Further reading: - The Temporal API in Node 26 - the big feature of this release, with migration from date-fns and dayjs. - nvm vs fnm vs Volta - the comfortable way to keep 24 and 26 side b

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.