Cannot find module: 6 Reasons Node.js Can't Find Your Module (and How to Fix Each)
Symptom: two error forms, one root cause
Error: Cannot find module 'express'
Require stack:
- /app/server.js
Or the ESM form:
Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'lodash' imported from /app/index.js
Both mean: Node can't locate the module file you asked it to load. Either the file doesn't exist, the path is wrong, or it's not installed.
Six causes, ranked by frequency
1. Not installed (most common for newcomers)
npm install express
Quick check: ls node_modules/express — does the directory exist?
2. Missing ./ prefix on relative paths
require('utils'); // ❌ Node looks in node_modules for a package named "utils"
require('./utils'); // ✅ relative paths must start with ./ or ../
The rule: any path not starting with ./, ../, or / goes to node_modules.
3. Corrupted node_modules (interrupted install, disk full)
rm -rf node_modules package-lock.json
npm install
Delete-and-reinstall fixes most weirdness — node_modules is ephemeral by design, reinstalling is lossless.
4. Case mismatch (works on macOS, breaks on Linux)
macOS filesystems are case-insensitive by default, so require('./Utils') finds utils.js. Linux (including Docker containers and CI servers) is case-sensitive — works locally, breaks in production is the classic symptom.
# Check actual casing
ls -la src/utils/ | grep -i util
5. ESM/CommonJS mismatch
After adding "type": "module" to package.json, all .js files are parsed as ESM:
| Syntax | CommonJS (.js) | ESM ("type":"module") |
|---|---|---|
| require('./x') | ✅ | ❌ ERR_MODULE_NOT_FOUND |
| import x from './x.js' | ❌ | ✅ (extension required) |
Two key ESM differences: import paths must include the file extension (./utils.js, not ./utils), and bare specifiers without extensions fail.
6. Monorepo / workspace path issues
In a monorepo referencing sibling packages, verify that package.json's workspaces includes the package, and that the import name matches the package's name field exactly.
Why deleting node_modules fixes 80% of cases
node_modules integrity depends on npm install running to completion. A network dropout, Ctrl+C, or disk-full during install can leave a half-installed state — the directory exists but the package's internal package.json (describing its own dependencies) is missing. Reinstalling rebuilds the entire tree, which is faster than surgical repair.
Prevention
- Use
npm ciin CI instead ofnpm install—ciinstalls strictly from the lock file and errors on mismatch instead of silently resolving - Run a type check before adding
skipLibCheck— rules out third-party type errors as a confounder - Use workspace protocol in monorepos (
"deps": {"@repo/utils": "workspace:*"}) instead of relative paths
The checklist
ls node_modules/<package>— does the directory exist?- Check path starts with
./(relative import) rm -rf node_modules && npm install— reinstall- Case sensitivity check (works locally → breaks on deploy = likely this)
"type": "module"check — does require vs import match the project type?
Provided by ToolVault. Related: Linux Cheatsheet, npm mirror guide, ERESOLVE conflicts. See the homepage for more developer tools.
Related Tools
Related Articles
Permission denied (publickey): 6 Reasons Git Push Fails Over SSH (and the Fix for Each)
git clone or push rejected with Permission denied (publickey) fatal: Could not read from remote repository? Covers missing keygen, key not loaded in the agent, public key not added to GitHub/GitLab, multi-account key routing with ~/.ssh/config, deploy key limits, and wrong remote URLs — with ssh -v diagnostics.
error:0308010C digital envelope routines::unsupported — Fixing Node 17+ Breakage in Older Projects
Node 17+ crashes webpack 4 builds with error:0308010C:digital envelope routines::unsupported because OpenSSL 3.0 removed MD4. Three fixes compared: --openssl-legacy-provider quick unblock, upgrading to webpack 5 as the real fix, and pinning Node 16 as a stopgap.
ECONNREFUSED: Connection Refused — 5 Causes Explained (Including Docker)
Node, Java, or curl reporting connect ECONNREFUSED 127.0.0.1:3306? It means nothing is listening on that port. Covers service not running, wrong port, 127.0.0.1-only binding, Docker container networking, and firewall REJECT rules — with ss/lsof diagnostic commands.