Fix: Node.js ERR_MODULE_NOT_FOUND - Cannot find module
Part of: JavaScript & TypeScript Errors
Quick Answer
How to fix Node.js ERR_MODULE_NOT_FOUND when using ES modules, covering missing file extensions, directory imports, package.json exports, TypeScript paths, and ESM resolution differences.
The Error
You run a Node.js script using ES modules and get:
node:internal/errors:496
ErrorCaptureStackTrace(err);
^
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/project/src/utils' imported from /project/src/index.js
at finalizeResolution (node:internal/modules/esm/resolve:260:11)Or a variation like:
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/project/src/utils/index.js'
imported from /project/src/app.js
Did you mean to import "../utils/index.js"?This error is specific to ES modules (ESM). It looks similar to the classic MODULE_NOT_FOUND error from CommonJS, but it has different rules and different fixes. If you are using require() instead of import, see Fix: Cannot find module (CommonJS).
Why This Happens
Node.js has two module systems: CommonJS (CJS) and ES Modules (ESM). They resolve imports differently, and the difference is more strict than most developers expect after years of writing CommonJS.
In CommonJS (require):
- File extensions are optional:
require('./utils')triesutils.js,utils.json,utils/index.js - Directory imports work:
require('./utils')resolves toutils/index.js
In ES Modules (import):
- File extensions are mandatory:
import './utils'does not try.jsautomatically - Directory imports do not work:
import './utils'does not resolve toutils/index.js - Package
exportsfield inpackage.jsontakes precedence over file system lookups
These stricter rules are by design — ESM follows the browser’s module resolution behavior, where URLs must include the full path. Browsers cannot guess file extensions or directory indexes because every guess is an HTTP round trip. Node.js applies the same rule even on local disk so that the same import string works in both runtimes. But the discipline catches many developers off guard when migrating from CommonJS, especially in mixed codebases where some files were written under the old rules and some under the new.
The error also has several variants that look almost identical but mean different things. ERR_MODULE_NOT_FOUND means the resolved file path simply does not exist on disk. ERR_PACKAGE_PATH_NOT_EXPORTED means the file exists but the package’s exports field forbids importing it. ERR_UNSUPPORTED_DIR_IMPORT is the explicit form that fires when you try to import a directory in ESM. Reading the exact error code first usually saves five minutes of guessing — every code points at a different fix.
In Production: Incident Lens
ESM resolution errors have a distinctive failure mode in production: the process refuses to start at all. Unlike runtime exceptions that surface only when traffic hits a specific code path, ERR_MODULE_NOT_FOUND is thrown during the synchronous import graph resolution at boot, so the Node process exits with code 1 before it ever serves a request. That changes how the incident looks on dashboards.
- How it surfaces: The deployment health check fails immediately after a release. Container orchestrators (ECS, Kubernetes, Fly, Cloud Run) report the new task as unhealthy and restart it in a tight loop. Logs show the stack trace once per restart. Users either see no impact (because the previous version is still serving) or a partial outage if the orchestrator started cutting over before catching the crash loop.
- Blast radius: Per-container at first, then the entire new deployment if the orchestrator promotes the broken image. A canary rollout limits the blast radius to a percentage of traffic; a blue-green or rolling deploy without health-check gating can take 100% offline.
- What catches it: Container restart count metrics, deployment health-check failures, and synthetic startup probes. A sharp uptick in restart count immediately after a release is the canonical signal. Sentry or Rollbar usually do not catch this because the error throws before the SDK is initialized.
- Recovery sequence: Rollback first, debug second. The previous image still works; redeploy it. Forward-fix only after you have reproduced the missing import locally and confirmed the exact
importline that fails. If the rollback signal is “new pods are crashlooping on startup,” do not try to ship a fix in 30 seconds — restore the old image. - Postmortem preventive: Add a startup smoke test to CI that runs
node --checkplus an actualnode -e "require('./dist/index.js')"(orimport()for ESM) against the built artifact. This catches missing files, missing extensions, and brokenexportsfields before the image is ever pushed. A canary deploy of one pod with strict health-check gating catches anything CI missed.
Fix 1: Add File Extensions to Imports
The most common fix. ESM requires explicit file extensions:
Broken:
import { helper } from './utils';
import { db } from '../lib/database';Fixed:
import { helper } from './utils.js';
import { db } from '../lib/database.js';Yes, even if the source file is .ts, the import must use .js when TypeScript compiles to JavaScript. TypeScript does not rewrite import paths during compilation.
This applies to all relative imports. Package imports (import express from 'express') do not need extensions — Node.js resolves those through node_modules.
Pro Tip: Use a linting rule to enforce file extensions in imports. The ESLint rule
import/extensionscan catch missing extensions at development time, before you hit the runtime error.
Fix 2: Fix Directory Imports
In CommonJS, require('./utils') automatically resolves to ./utils/index.js. In ESM, this does not work:
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/project/src/utils'Fix: Import the index file explicitly:
import { helper } from './utils/index.js';Or create a package.json inside the directory with an exports field:
{
"exports": "./index.js"
}Then the bare directory import works:
import { helper } from './utils';Fix 3: Configure package.json exports
The exports field in package.json controls how your package’s modules are resolved. If it is misconfigured, imports fail with ERR_MODULE_NOT_FOUND.
A typical exports configuration:
{
"name": "my-package",
"type": "module",
"exports": {
".": "./src/index.js",
"./utils": "./src/utils/index.js",
"./helpers/*": "./src/helpers/*.js"
}
}This allows:
import { main } from 'my-package'; // resolves to ./src/index.js
import { helper } from 'my-package/utils'; // resolves to ./src/utils/index.js
import { foo } from 'my-package/helpers/foo'; // resolves to ./src/helpers/foo.jsCommon issues:
- Missing the
"."entry — the main entry point is not exported - Forgetting to include subpath patterns —
import 'my-package/utils'fails - File paths in
exportsdon’t match actual file locations
If the exports field exists, it completely overrides the main and module fields. Node.js ignores those when exports is present.
If module resolution errors happen during bundling rather than Node.js runtime, the issue lies in your bundler config, not Node.js itself.
Fix 4: Fix TypeScript Output Paths
TypeScript compiles .ts files to .js files, but it does not rewrite import paths. This means:
// src/index.ts
import { helper } from './utils'; // Works in TypeScript...Compiles to:
// dist/index.js
import { helper } from './utils'; // ...but fails in Node.js ESMFix option 1: Use .js extensions in TypeScript source files:
import { helper } from './utils.js'; // Yes, .js even in .ts filesThis looks strange but is the recommended approach. TypeScript understands that ./utils.js refers to ./utils.ts during compilation.
Fix option 2: Use a build tool that rewrites paths:
Tools like tsup, unbuild, or esbuild can bundle your TypeScript and handle path resolution:
npx tsup src/index.ts --format esmFix option 3: Use tsx to run TypeScript directly:
npx tsx src/index.tstsx handles ESM resolution automatically, including file extensions and directory imports. It is ideal for development and scripts.
If TypeScript itself cannot find your modules at compile time (rather than at runtime), the cause is usually a tsconfig.json moduleResolution or paths misconfiguration, not an ESM resolution issue.
Fix 5: Replace __dirname and __filename
In CommonJS, __dirname and __filename are global variables. In ESM, they do not exist:
console.log(__dirname);
// ReferenceError: __dirname is not defined in ES module scopeFix: Use import.meta.url:
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Now you can use __dirname as before
const configPath = join(__dirname, 'config.json');In Node.js 21.2+, you can use import.meta.dirname and import.meta.filename directly:
const configPath = join(import.meta.dirname, 'config.json');Fix 6: Handle JSON Imports
ESM does not import JSON files by default:
import config from './config.json';
// TypeError [ERR_IMPORT_ATTRIBUTE_MISSING]: Module needs an import attribute of "type: json"Fix: Add the import attribute (Node.js 20.10+):
import config from './config.json' with { type: 'json' };For older Node.js versions, use assert instead of with:
import config from './config.json' assert { type: 'json' };Or read the JSON file manually:
import { readFileSync } from 'node:fs';
const config = JSON.parse(readFileSync(new URL('./config.json', import.meta.url), 'utf-8'));Fix 7: Fix node_modules Package Resolution
If the error points to a package in node_modules:
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '.../node_modules/some-package/lib/index.mjs'The package might have a broken exports field or missing files.
Step 1: Reinstall the package:
rm -rf node_modules package-lock.json
npm installStep 2: Check if the package supports ESM:
npm info some-packageLook for "type": "module" or an exports field in the package’s package.json. Some older packages only support CommonJS.
Step 3: Use a dynamic import for CJS-only packages:
const pkg = await import('cjs-only-package');Dynamic import() works with both ESM and CJS packages.
If you’re getting a similar error but with import syntax being rejected entirely, see Fix: Cannot use import statement outside a module.
Fix 8: Check for Case Sensitivity
On Linux, file paths are case-sensitive. import './Utils.js' and import './utils.js' are different:
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/project/src/Utils.js'Check the actual filename:
ls src/Match the import exactly to the file’s case. This is a common issue when developing on macOS (case-insensitive) and deploying to Linux (case-sensitive).
If the error happens during a bundler build (Vite, webpack, esbuild) rather than at Node.js runtime, the fix path is different — bundlers have their own resolution algorithms.
Still Not Working?
If none of the fixes above resolved the error:
Check your package.json has "type": "module". Without this, Node.js treats .js files as CommonJS, and import syntax fails with a different error. Add it:
{
"type": "module"
}Check your Node.js version. Full ESM support requires Node.js 14+ (with --experimental-modules flag) or Node.js 16+ (stable). Update to the latest LTS version:
nvm install --ltsCheck for symlink issues. npm link and pnpm use symlinks. ESM resolves paths differently with symlinks. Try --preserve-symlinks:
node --preserve-symlinks index.jsCheck for import map issues. If you use --import or custom loaders, they can interfere with module resolution. Try running without them first.
Use --trace-warnings for more detail:
node --trace-warnings index.jsThis shows the full stack trace for resolution errors, which helps pinpoint exactly which import is failing.
Check the exact error code. ERR_MODULE_NOT_FOUND means the file path doesn’t exist. The related ERR_PACKAGE_PATH_NOT_EXPORTED means the file exists but isn’t listed in the package’s exports field — a different fix entirely.
Check for missing files after npm prune --production. Production deploys often strip dev dependencies. If your entry point transitively imports something only present in devDependencies (a typing-only import that survived TypeScript compilation, a test helper imported by mistake), the runtime resolution fails after the prune step succeeds. Reproduce locally with npm ci --omit=dev and run the built artifact before deploying.
Check Docker image layer ordering. A common production failure: COPY package*.json ./ and RUN npm ci succeed, but COPY . . later overwrites node_modules or dist with a stale local copy. Always add node_modules and dist to .dockerignore, and verify the final image with docker run --rm <image> ls -la /app/node_modules before promoting.
Check for monorepo workspace symlinks. In pnpm or Yarn workspaces, internal packages are linked, not copied. If your Docker build copies one workspace package without the linked dependencies, the imports break. Use the workspace tool’s deploy command (pnpm deploy, yarn workspaces focus --production) to produce a self-contained directory before building the image.
Check for node --experimental-vm-modules or ESM loaders. Some test runners (Jest, Vitest) and tools (ts-node/esm, tsx) use custom loaders that interpret extension rules differently than vanilla Node. An import that works under tsx can fail under node because the loader was masking a real bug. Always run the built artifact with plain node before deploying.
For module errors specific to bundlers or build tools, see Fix: Module not found: Can’t resolve for webpack and similar setups. For TypeScript compile-time module errors, see Fix: TypeScript Cannot find module.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Node.js fs.watch Not Working — Cross-Platform Quirks, chokidar Migration, Recursive Watch
How to fix Node.js file watching — fs.watch unreliable on Linux, missing events on save, recursive watch on Windows/macOS, chokidar polling fallback, ignoring patterns, debouncing, and EMFILE errors.
Fix: Node.js Stream pipeline() Not Working — Backpressure, Error Propagation, AbortSignal, and Web Streams Interop
How to fix Node.js stream/promises pipeline errors — uncaught stream errors, backpressure ignored, AbortSignal not propagating, async iterators in pipeline, Transform stream object mode, and converting between Node and Web Streams.
Fix: Node.js Test Runner Not Working — node --test, TypeScript, Mocks, Coverage, and Watch Mode
How to fix Node.js built-in test runner errors — node --test not finding files, ESM vs CJS imports, TypeScript with --experimental-strip-types, mock.method isolation, coverage reporting, and watch mode setup.
Fix: tsx Not Working — ESM Imports, Watch Mode, Path Aliases, and node --import tsx
How to fix tsx (TypeScript executor) errors — Cannot find module after upgrade, ESM .js extension required, tsconfig paths not respected, watch mode not restarting, --import vs --loader, VS Code debugger setup.