Skip to content
DevOps2026-09-086 min read

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:

SyntaxCommonJS (.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

  1. Use npm ci in CI instead of npm install — ci installs strictly from the lock file and errors on mismatch instead of silently resolving
  2. Run a type check before adding skipLibCheck — rules out third-party type errors as a confounder
  3. Use workspace protocol in monorepos ("deps": {"@repo/utils": "workspace:*"}) instead of relative paths

The three invisible causes beyond the checklist

(1) Case-sensitivity drift: code developed on macOS (case-insensitive) referencing ./utils/Logger when the file is logger.js — works locally, dies on the Linux CI/container. The one-command audit: run the project in a case-sensitive disk image or use tsconfig's forceConsistentCasingInFileNames before the CI finds it for you. (2) Exotic characters in the path: spaces and non-ASCII in the project directory break some bundlers' module resolution on Windows — the error appears as module-not-found with no hint the path is the culprit. (3) The module resolves but throws during evaluation — require() reports it as "Cannot find module" in some stacks when the real error is inside the module file (a syntax error, a failed import of its own). Reading the stack's innermost frame, not the first line, separates these instantly.

ESM vs CJS: when the same fix book says different things

require() resolves .js with implicit extensions and directory index.js; ES imports want explicit-enough specifiers and type: module or .mjs. A CJS fix (adding to package.json dependencies) is useless if the file is ESM and the package exports map does not expose the subpath you import — the exports map is the gatekeeper modern packages use, and importing pkg/lib/internal that is not in the map fails as not-found even though the file sits in node_modules. The diagnostic split: run node -e "console.log(require.resolve('pkg'))" (CJS) versus node --input-type=module -e "import('pkg').then(console.log)" — the one that works tells you which resolution world you are in.

Postmortem template for the recurring case

When the same module error returns monthly, the fix was symptom-level. The durable template: reproduce with a two-line test file (imports the module, prints ok) — if the test file works but the app does not, the problem is configuration (paths, tsconfig baseUrl, bundler alias), not the module. Pin that test into CI; the next drift fails a five-second check instead of a debugging afternoon.

TypeScript paths vs runtime resolution

tsconfig.json paths / baseUrl only teach the TypeScript compiler (and sometimes the bundler) where to look. Node itself does not read those aliases at runtime unless you add a loader (tsx, ts-node, module-alias) or compile them away with a bundler. The classic failure: import '@/lib/x' type-checks in the IDE, then node dist/server.js throws ERR_MODULE_NOT_FOUND because the emitted JS still contains @/. Fix by either rewriting aliases at build time or switching runtime imports to relative / package exports. When debugging, open the compiled file — if the alias is still there, the problem is the build pipeline, not npm.

Conditional exports and dual packages

Modern packages ship an exports map with import / require / default conditions. Importing a subpath that is not listed fails as “not found” even when the file exists under node_modules. Dual CJS/ESM packages can also expose different entry files per condition; requiring the ESM-only entry from CJS (or the reverse) surfaces as module-not-found or ERR_REQUIRE_ESM. Check node -p "require('pkg/package.json').exports" (or open the file) before filing a bug against the package.

The checklist

  1. ls node_modules/<package> — does the directory exist?
  2. Check path starts with ./ (relative import)
  3. rm -rf node_modules && npm install — reinstall
  4. Case sensitivity check (works locally → breaks on deploy = likely this)
  5. "type": "module" check — does require vs import match the project type?
  6. Compiled output: are paths aliases rewritten, and is the subpath allowed by exports?

Triage notes

The six causes are ordered by likelihood, but the third — delete node_modules and reinstall — gets its own explanation of why it fixes 80% of cases: a half-installed state left by an interrupted install is cheaper to rebuild from scratch than to repair precisely, and since node_modules is a disposable artifact, reinstalling loses nothing. The case-sensitivity entry is flagged as the classic "works locally, breaks on deploy" because macOS filesystems are case-insensitive by default while Linux, containers, and CI are not. The three hidden causes beyond the checklist share one piece of advice: read the bottom frame of the stack, not the first line of the message — some toolchains report a module's own syntax error as Cannot find module.


Provided by ToolVault. Related: Linux Cheatsheet, npm mirror guide, ERESOLVE conflicts, Node 17+ OpenSSL error. See the homepage for more developer tools.