Skip to content
Nilay Kabariya

Entry No.076·Fixes·Updated ·3 min read

The punycode module is deprecated (DEP0040): why only builds show it

Node 20 never prints DEP0040; Node 22+ prints it for your own code and bundles, not node_modules. Usually node-fetch 2 via whatwg-url, not tough-cookie or ajv.

The error, verbatim

Jump to the fix ↓
(node:24108) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
(Use `node --trace-deprecation ...` to show where the warning was created)
✓ Reproduced on Ubuntu on GitHub Actions; Windows 11 Pro

Tested on

Node
20.20.2, 22.23.4, 24.22.1, 26.13.0 (Linux); 24.14.0, 26.11.1 (Windows)
Packages
node-fetch 2.7.0, whatwg-url 5.0.0, tr46 0.0.3 / 5.1.1, tough-cookie 4.1.4, psl 1.15.0, ajv 6.15.0
Bundler
esbuild 0.28
OS
Ubuntu on GitHub Actions; Windows 11 Pro

✓ Last re-run on GitHub Actions · see the run · all repros

Contents
  1. Find the package01
  2. The fix: stop using node-fetch 202
  3. What didn’t work03
  4. How this was tested04

Two things decide whether you see this warning, and neither is your code: the Node version and whether you bundle. On Node 20 it never appeared, even for a direct require('punycode'). On Node 22, 24 and 26 it appeared for code outside node_modules, which includes anything a bundler copied into your build, and that’s why it shows up in production logs but not in development. It’s only a warning, and the program keeps running.

The odd part is when you see it. On Node 24.14.0:

What ran Warning?
Your own code calling require('punycode') Yes
node-fetch 2 from node_modules, actually fetching No
The same code bundled into dist/app.js with esbuild Yes

Node only prints it for code outside node_modules. A package that loads punycode stays quiet in development, then the warning appears once a bundler copies that package into your build output. That’s why it so often shows up in production logs, serverless functions and CLI builds, from code you never wrote. Unbundled node-fetch 2 stayed silent on Node 26.11.1 as well.

Which Node versions print it

Node 20.20.2 Node 22.23.4 Node 24.22.1 Node 26.13.0
Your own require('punycode') no warning warning warning warning
node-fetch 2 bundled with esbuild no warning warning warning warning
node-fetch 2 from node_modules no warning no warning no warning no warning

So the warning usually appears right after moving from Node 20 to 22 or newer, with no change to your code. Node 20 loads the same module silently.

Which packages cause it

I bundled each of these on its own and ran the result on Node 22, 24 and 26:

Package Bundled: DEP0040?
node-fetch 2.7.0 yes
whatwg-url 5.0.0 (used by node-fetch 2) yes
tough-cookie 4.1.4 no
psl 1.15.0 no
ajv 6.15.0 (through uri-js) no

The last three use the punycode package from npm, which npm ls punycode lists, rather than Node’s built-in module, so they’re not the cause even though the name matches. Their bundles contained no require("punycode") of the built-in at all; the node-fetch 2 and whatwg-url 5 bundles each contained two.

Find the package

--trace-deprecation isn’t much help here: the stack starts at node:punycode:7:10, inside Node, not in your code.

The usual source is node-fetch 2, through whatwg-url 5 and tr46 0.0.3. Check whether you have it:

npm ls tr46 whatwg-url
`-- node-fetch@2.7.0
  `-- whatwg-url@5.0.0
    `-- tr46@0.0.3

If that chain shows up, node-fetch 2 is the one, either yours or a dependency’s (older SDKs often use it).

The fix: stop using node-fetch 2

Node 18 and later have fetch built in. Remove node-fetch and use the global one:

const res = await fetch('https://registry.npmjs.org/is-number');

If the code needs the node-fetch API, node-fetch 3 is ES-module only and doesn’t use whatwg-url 5. It fetched fine on Node 24 (status 200). If a dependency pulls in node-fetch 2, update that dependency.

What didn’t work

node --no-deprecation hides every deprecation warning, this one included. It changes nothing about the code, and it also hides the next warning you’d want to see.

How this was tested

Windows 11 Pro, Node 24.14.0 and 26.11.1, October 18, 2026. A one-line script fetching from the npm registry with node-fetch 2.7.0, run directly and as an esbuild 0.28.2 bundle (--bundle --platform=node), then with a tr46 override, a punycode alias, node-fetch 3 and native fetch. Every message above is the real output.

November 1, 2026: own-code, unbundled and esbuild-bundled runs on Node 20.20.2, 22.23.4, 24.22.1 and 26.13.0 on GitHub Actions ubuntu-latest, for node-fetch 2.7.0, whatwg-url 5.0.0, tough-cookie 4.1.4, psl 1.15.0 and ajv 6.15.0. The script and the full output are public.

— N.K., end of entry No.076

Useful? Pass it on:Post on XFollow @EmotionalMatter

Related entries

  1. No.002

    Fix: require, __dirname not defined in ES module scope

    ReferenceError: require, __dirname or __filename is not defined in ES module scope. Why Node says it, and the fixes, tested on Node 24 and 26.

    > ReferenceError: __dirname is not defined in ES module scope

    FIXED3 min
  2. No.093

    Fix: bigint Failed to load bindings, pure JS will be used

    bigint: Failed to load bindings, pure JS will be used. Why npm 12 shows it on every install, whether it matters, and how to get the native build back.

    > bigint: Failed to load bindings, pure JS will be used (try npm run rebuild?)

    FIXED3 minWindows
  3. No.091

    Fix: npm error code EALLOWREMOTE (and EALLOWGIT), tested

    npm error code EALLOWREMOTE: Fetching packages of type remote have been disabled. Every case reproduced on npm 12, from npx to npm ci, with each fix.

    > npm error code EALLOWREMOTE

    FIXED4 min
  4. No.086

    npm error code EACCES: permission denied, 3 causes tested

    npm error code EACCES with syscall mkdir or open: a global install, a cache that sudo left behind, or Docker. Each reproduced, with the fix that lasts.

    > npm error code EACCES

    FIXED2 min

Post card · Every Sunday

What broke this week.

One email on Sundays: the Node, TypeScript, Python and AI SDK releases that broke builds that week, each one reproduced, with a link to the fix that worked. Never shared, and you can leave any time.

— Nilay

or follow by RSSor on X

By subscribing you agree to the privacy note. One click to leave.

tip: paste the exact error text