Quick take: - Node.js 22 and 24 are both LTS releases; v20 maintenance ends April 2026. The biggest breaking change is
require()behavior for ES modules, v22 added experimental support, v23 unflagged it. Other removals that will bite you:crypto.createCipher/createDecipher, deprecatedurl.parse()patterns, and some internalprocess.binding()calls. New gains: stable nativefetch,fs.glob(), TypeScript type stripping, and a built-in WebSocket client, breaks is covered here.
Node.js uses an even-numbered LTS schedule: v20 maintenance ends April 2026, v22 is the current active LTS (active through October 2026), v24 dropped in April 2025. The biggest change between v20 and v22 is require() behavior for ES modules, v22 added experimental support behind --experimental-require-module, v23 unflagged it. Files with top-level await still throw ERR_REQUIRE_ASYNC_MODULE. Two hard removals: crypto.createCipher() and crypto.createDecipher() are gone in v22; migrate to createCipheriv() with an explicit IV. New capabilities in v22: native fetch is stable (drop node-fetch), fs.glob() and fs.promises.glob() handle basic file patterns natively, --experimental-strip-types lets .ts files run directly without a build step (no enums or decorators), and the global WebSocket constructor matches the browser API for client connections. When migrating, update @types/node to match your target version and test on v22 before jumping to v24.
Node.js follows a predictable LTS schedule, even-numbered versions (18, 20, 22, 24) are Long-Term Support. If you're on v20, its active LTS period ended October 2024. It gets security patches until April 2026, then nothing. V22 is the current active LTS; v24 dropped in April 2025. I've migrated four Node.js services from v20 to v22 over the past year. Here's what actually caused problems.
What Does the LTS Timeline Look Like?
Don't jump from v20 straight to v24 without testing on v22 first. The bulk of breaking changes landed in v22, that's where things break in your CI.
| Version | Status | Active LTS End | Maintenance End |
|---|---|---|---|
| Node.js 18 | End of life | October 2023 | April 2025 |
| Node.js 20 | Maintenance | October 2024 | April 2026 |
| Node.js 22 | Active LTS | October 2026 | April 2027 |
| Node.js 24 | Current | October 2027 | April 2028 |
V20 will receive security patches until April 2026. That's your real deadline, not today.
The Biggest Change: require() and ES Modules
This is the one that broke something in every codebase I touched. Node.js v22 introduced experimental support for require() of synchronous ES modules behind the --experimental-require-module flag. Node.js v23 unflagged it, no flag needed.
Here's what that means in practice:
// Node.js 20, throws ERR_REQUIRE_ESM
const utils = require('./utils.mjs');
// Node.js 23+, works if utils.mjs has no top-level await
const utils = require('./utils.mjs');
The catch: files with top-level await still throw, and the error is clear:
Error [ERR_REQUIRE_ASYNC_MODULE]: require() of an ES Module
that uses top-level await is not supported.
Fix for v22: Add --experimental-require-module to your start script while testing. For production on v23+, nothing extra needed.
Fix for top-level await: Move that logic into an async factory function instead. If you want the module to be require()-able, it can't have top-level await.
TypeScript projects using "module": "NodeNext", check your package.json type field. If it's "type": "module", all .js output is treated as ESM. This is now a feature rather than a problem on v23+, but your team needs to understand it.
Breaking: crypto.createCipher and createDecipher Removed
These were deprecated since Node.js v10, but a lot of code still uses them. They're gone in v22:
// Throws in v22: TypeError: crypto.createCipher is not a function
const cipher = crypto.createCipher('aes192', password);
Fix: Migrate to createCipheriv, which requires an explicit initialization vector. That's the secure version anyway:
import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
const iv = randomBytes(16);
const key = Buffer.from(yourKey, 'hex'); // 32 bytes for AES-256
const cipher = createCipheriv('aes-256-cbc', key, iv);
Store the IV alongside the ciphertext. You'll need it to decrypt. The crypto.createCipher API derived the key from a password insecurely, this migration also fixes a real security issue.
The node: Prefix Becomes the Convention
Built-in modules work with or without the node: prefix in v22. But tooling, ESLint's unicorn ruleset, type-checking tools, import analyzers, increasingly expects it. It also eliminates any ambiguity between a built-in and an npm package with the same name:
// Both work, but node: is the current convention
import fs from 'node:fs/promises';
import path from 'node:path';
import crypto from 'node:crypto';
Is this a breaking change? Not technically, your old imports still work. But if you're adding the unicorn/prefer-node-protocol ESLint rule (which the Airbnb config now includes), your linter will fail CI until you update them.
Fix: Run a quick sed over your source or use ESLint's --fix flag:
npx eslint src --rule "unicorn/prefer-node-protocol: error" --fix
New: fs.glob(), Drop the glob Package
Node.js v22 ships fs.glob() and fs.promises.glob() natively. No more glob as a dependency for basic file matching:
import { glob } from 'node:fs/promises';
for await (const file of glob('src/**/*.ts')) {
console.log(file);
}
// Or collect into array
const files = await Array.fromAsync(glob('src/**/*.ts'));
The npm glob package still works and supports more advanced options. Replace it only after verifying your specific patterns match. Simple **/*.ext patterns work fine natively.
New: TypeScript Type Stripping
Node.js v22.6.0 added --experimental-strip-types. By v24, you can run .ts files directly for many use cases:
# v22.6+
node --experimental-strip-types scripts/seed.ts
# v23+
node --experimental-strip-types src/server.ts
Limitations are real: no enum, no namespace, no experimentalDecorators, no path aliases. The flag strips type annotations but doesn't transform TypeScript syntax. For scripts without those features, it's genuinely useful, I've switched our database seed scripts to run this way and removed their separate build step.
For production services, tsx or ts-node are still more reliable. The feature is worth watching for v24's final LTS release. For a deeper look at what native TypeScript support actually means, what it handles, what it skips, and when to still reach for ts-node, see Node.js native TypeScript without ts-node.
New: Built-in WebSocket Client
Node.js v22 added a global WebSocket constructor matching the browser API, no ws package needed for client connections:
// No npm install ws
const socket = new WebSocket('wss://stream.example.com');
socket.addEventListener('open', () => {
socket.send(JSON.stringify({ type: 'subscribe', channel: 'prices' }));
});
socket.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
});
If you use ws only as a WebSocket client (not server), you can remove it for v22+ targets. WebSocket server functionality still requires the ws package.
Migration Checklist
Before bumping the Node.js version in your Dockerfile or .nvmrc:
- Search for
crypto.createCipherandcrypto.createDecipher, migrate tocreateCipheriv - Check dynamic
require()of.mjsfiles, test on v22 with--experimental-require-module - Search for
url.parse(), migrate tonew URL(urlString, baseUrl) - Update
@types/nodeto match your target:npm install -D @types/node@22 - Run your full test suite on v22 in CI before touching prod
- Update the
enginesfield inpackage.json:
{
"engines": {
"node": ">=22.0.0"
}
}
That last step gets skipped constantly. It prevents your package from being accidentally run under an older Node.js by a fresh install on a different machine.
Related
- TypeScript 7 Migration Guide, if you're upgrading Node, this is a good time to evaluate the native Go TypeScript compiler too
- React Server Components with TypeScript, Next.js 14+ recommends Node.js 22 as the minimum for production RSC deployments
- TypeScript Generics Guide,
@types/nodegenerics forfs.glob()async iterators and the new crypto APIs - Three Pillars of JavaScript Bloat, dropping
globandwsdependencies is one concrete step toward reducing bundle weight - Bun vs Node vs Deno 2026, once you're on Node 24 LTS, this benchmark shows where Bun and Deno actually beat it and where they don't