Diagnosing ERR_MODULE_NOT_FOUND after upgrading Node
Node's ESM resolver rules around mandatory file extensions and 'exports' enforcement are not new in Node 22 — the summary is important here: the upgrade is usually the trigger only because it came with a lockfile/dependency refresh, not because Node itself changed how it resolves modules. Splits the failing specifier into your own relative import (missing extension) versus a dependency (its exports map tightened). It does not cover a plain 'Cannot find module' with no ERR_ prefix right after npm install — that's a CommonJS-style resolution failure covered by a separate playbook.
Started immediately after upgrading Node.js (for example from 20 to 22)
Worked before the upgrade with no code changes to the failing import
The diagnostic path
6 steps. Every step is written out below in full — the interactive version simply follows the branches for you.
Step 1 · StartERR_MODULE_NOT_FOUND after upgrading Node
Node's mandatory-extension and package.json 'exports' rules under "type": "module" have been in place since ESM support landed, well before Node 20 or 22 — the version upgrade is usually the trigger only because it shipped with a dependency/lockfile update, not because Node's own resolution rules changed.
What happens next
passed → step 2, Does the failing specifier point at your own file, or an installed package?
Step 2 · TestDoes the failing specifier point at your own file, or an installed package?
Read the error's 'Cannot find module ...' line: a specifier starting with ./ or ../ is your own code; a bare name is a dependency.
Read-onlysh
# read the exact specifier from the error text
Expected result
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/app/src/utils' imported from /app/src/index.js
What happens next
passed → step 3, Fix: add the required file extension to the relative import
failed → step 4, Check the dependency's package.json 'exports' field for the path being imported
unknown → step 4, Check the dependency's package.json 'exports' field for the path being imported
Step 3 · FixFix: add the required file extension to the relative import
Node's ESM loader requires the extension on relative specifiers — import { thing } from './utils' fails under "type": "module" even if it worked under CommonJS or a bundler that resolved extensions for you. Change it to './utils.js' (or whatever the actual compiled output extension is). If this previously ran through a build/transpile step that isn't running any more after the upgrade, that step disappearing is the real trigger, not Node 22 itself.
Step 4 · TestCheck the dependency's package.json 'exports' field for the path being imported
A newer version of the dependency may have added or tightened an 'exports' map, which blocks any import path not explicitly listed — including deep imports that used to work.
passed → step 5, Root cause: the dependency's exports map no longer allows this import path
failed → step 6, The exports map isn't blocking this
unknown → step 6, The exports map isn't blocking this
Step 5 · Root causeRoot cause: the dependency's exports map no longer allows this import path
The subpath being imported genuinely isn't listed in the package's 'exports' map. Import only the paths it actually exposes (usually the package root or a documented subpath), or pin the dependency to the last version before 'exports' was added or tightened if the internal path is genuinely required.
Step 6 · EndThe exports map isn't blocking this
The path being imported is listed in 'exports', or the package has no 'exports' field at all. Check instead whether the package actually installed correctly for this Node version (npm ls <package-name> for peer-dependency warnings), and whether two copies of it exist at different versions in node_modules, where a stale nested copy can shadow the top-level one.