Skip to content

Node.js 18 → 22 Migration Guide

Official Changelogs

Key Breaking & Impactful Changes

1. process.exit() / process.exitCode no longer coerce strings

Before (Node 18):

process.exit("1"); // coerced to 1
process.exitCode = "0"; // coerced to 0

After (Node 20+): These must be integers; strings, null, or undefined will throw an error when passed to process.exit(), or behave unexpectedly when assigned to process.exitCode.

process.exit(1); // ✅ correct
process.exitCode = 0; // ✅ correct

Fix: Audit and replace all usages where a non-integer (especially string) is passed to process.exit() or assigned to process.exitCode.

2. url.parse() with invalid ports now emits a runtime warning

Before (Node 18):

const { parse } = require("url");
parse("http://example.com:badport"); // silently fails

After (Node 20+): Emits a deprecation warning. This legacy API will throw in future versions.

Fix: Replace with the URL API, which throws properly for invalid URLs:

new URL("http://example.com:badport"); // will throw properly

3. Native modules must be rebuilt for Node.js 22

Node.js 22 ships with a newer version of V8 and ABI (Application Binary Interface). Native dependencies like sharp, bcrypt, node-sass, etc., must be rebuilt to be compatible.

Fix:

rm -rf node_modules package-lock.json # Or yarn.lock
npm install
npm rebuild # Ensures native modules are rebuilt for the current Node.js version

4. Global fetch, WebStreams, and FormData are now built-in and stable

Node.js 18 had these as experimental. Node.js 20+ includes them as stable and native, based on the undici library for fetch.

Fix: If you're using libraries like node-fetch, web-streams-polyfill, or custom polyfills/mocks for these APIs, remove them. The native implementations should be used. Be aware of minor behavioral differences (e.g., default timeouts) compared to node-fetch.

5. ECMAScript Module (ESM) Resolution Behavior

Node.js 20+ has improved ESM loader and error messages, and stricter handling of package.json "exports" and "type" fields.

  • Impact: Modules might resolve differently, or throw errors for previously tolerant import paths. Conditional exports in package.json ("node", "import", "require") are more strictly enforced.
  • Stability: Experimental flags like --experimental-json-modules and --experimental-wasm-modules are now stable and can be removed.

Fix: If using ESM, test your imports and module resolution carefully. Review your package.json "type" field and "exports" map. Ensure all module paths work correctly.

6. npm's bin scripts in package.json

  • Change: npm no longer implicitly executes bin scripts (like eslint, mocha) directly from package.json scripts.
  • Impact: If your package.json scripts call local binaries without npx or a full path, they will fail.
  • Fix: Prepend npx to the command, or use the full path.
  • Before: "test": "mocha"
  • After: "test": "npx mocha" or "test": "./node_modules/.bin/mocha"

7. fs.watch() throws on unsupported platforms

  • Change: Calling fs.watch() on platforms where it's not supported now throws a TypeError. Previously, it might have failed silently or behaved unexpectedly.
  • Impact: If your app code uses fs.watch() in an environment that doesn't fully support it, this will now cause a runtime error.
  • Fix: Audit code for fs.watch() usage. Ensure it's only used where truly needed and supported.

8. http.Server Options Validation

  • Change: Passing an array as options to http.Server (e.g., new http.Server([])) will now throw an error. Previously, it might have been coerced or ignored.
  • Impact: If you had a bug in your server instantiation where an array was accidentally passed instead of an object, it will now explicitly break.
  • Fix: Ensure the options argument for http.Server is always a plain object.

9. Stricter Stream defaultEncoding Validation

  • Change: Readable and Writable streams will now throw an error if an invalid defaultEncoding is provided during instantiation or configuration.
  • Impact: If your code was using an invalid or unsupported encoding string for streams, it will now fail.
  • Fix: Ensure valid encoding strings (e.g., 'utf8', 'base64', 'hex') are used for defaultEncoding.

10. Crypto Legacy API Deprecations

  • Change: The legacy crypto.createCipher(), crypto.createDecipher(), and direct new crypto.Hash() and new crypto.Hmac() constructors are now runtime deprecated or end-of-life (EOL).
  • Impact: Using these methods will emit runtime deprecation warnings. They are generally considered less secure or outdated and will be removed in future major versions.
  • Fix: Migrate to modern, recommended crypto module functions such as crypto.createCipheriv(), crypto.createDecipheriv(), crypto.createHash(), and crypto.createHmac().

11. util.is* Type-Checking Function Deprecations

  • Change: Most util.is* type-checking functions (e.g., util.isString(), util.isNumber(), util.isArray(), etc.) are now runtime deprecated. Also, util.log() and util._extend() are deprecated.
  • Impact: Using these functions will emit runtime deprecation warnings. They should be replaced with native JavaScript alternatives.
  • Fix: Replace deprecated util.is* functions with modern JavaScript constructs:
  • util.isString(value)typeof value === 'string'
  • util.isNumber(value)typeof value === 'number'
  • util.isArray(value)Array.isArray(value)
  • util.isObject(value)typeof value === 'object' && value !== null
  • util.isBuffer(value)Buffer.isBuffer(value)
  • util.isNull(value)value === null
  • util.isUndefined(value)typeof value === 'undefined'
  • util.isNullOrUndefined(value)value == null
  • util.log(message)console.log(message)
  • util._extend(target, source)Object.assign(target, source)

Behavioral Changes

  • net.autoSelectFamily default (Node.js 20): net.connect() and net.createServer() now have autoSelectFamily set to true by default, which means Node.js will attempt to connect to both IPv4 and IPv6 addresses. Most applications will see no change or benefit, but if you rely on specific address family behavior, be aware.
  • Stream highWaterMark default (Node.js 22): The default highWaterMark for streams has been increased. This means streams might buffer more data by default before backpressure signals are sent, potentially affecting memory usage or backpressure mechanisms if your application is sensitive to these defaults.

Migration Steps

Step 1: Update Your App Manifest

Update your app.yml file to specify Node.js 22:

nodeRuntime: "nodejs22.x"

Step 2: Codebase Audit

Search for these patterns in your codebase:

grep -r "process.exit" .
grep -r "process.exitCode" .
grep -r "url.parse" .
grep -r "node-fetch" .
grep -r "bycrypt" .     # Example for native modules
grep -r "http.createServer" .
grep -r "new Readable" .
grep -r "new Writable" .
grep -r "createCipher" .
grep -r "createDecipher" .
grep -r "new crypto.Hash" .
grep -r "new crypto.Hmac" .
grep -r "util.is" .
grep -r "util.log" .
grep -r "util._extend" .
grep -r "fs.watch" .

Review package.json scripts for local binary calls without npx (e.g., "eslint file.js" instead of "npx eslint file.js").

Step 3: Test Locally

Use nvm to test your app with Node.js 22:

nvm install 22
nvm use 22
npm install # Ensure all dependencies are updated and rebuilt
npm test

Step 4: Update Dependencies

Rebuild native modules and update dependencies:

rm -rf node_modules package-lock.json
npm install
npm rebuild

Summary of Required Code Changes

Area Change
process.exit() Use only integers
process.exitCode Use only integers
url.parse() Replace with new URL()
Native modules Rebuild under Node.js 22
fetch / FormData Remove polyfills, use native implementations
ESM Ensure correct exports/type, remove experimental flags
package.json scripts Use npx for local binaries
fs.watch() Audit and ensure usage on supported platforms
http.Server Options must be an object
Streams Ensure valid defaultEncoding
Crypto (legacy APIs) Migrate to modern alternatives
util.is* functions Replace with native JS type checks
util.log, util._extend Replace with console.log, Object.assign()

After completing these changes, thoroughly test your app in a development org before installing it on production orgs.