Error [ERR_MODULE_NOT_FOUND]: Cannot find module './utils' imported from /app/index.js when the ESM loader cannot resolve an import specifier. The #1 cause is a missing .js extension in a relative import — ESM never adds extensions automatically. Fix: change import { x } from './utils' to import { x } from './utils.js'. In TypeScript, set "module": "Node16" and "moduleResolution": "Node16" in tsconfig.json so the compiler enforces this for you.
What is ERR_MODULE_NOT_FOUND?
ERR_MODULE_NOT_FOUND is thrown exclusively by Node.js's native
ES module (ESM) loader
when an import statement or import() call cannot be resolved to a file
or package. It is the ESM counterpart to the CommonJS
MODULE_NOT_FOUND code.
The critical difference: the ESM loader is strict. It does not automatically try
adding .js, look for /index.js in directories, or fall back to other
extensions. Every specifier must exactly name a resolvable file.
Error [ERR_MODULE_NOT_FOUND]: Cannot find module './utils' imported from /app/index.jsError [ERR_MODULE_NOT_FOUND]: Cannot find module './helpers/format' imported from /app/src/index.jsError [ERR_MODULE_NOT_FOUND]: Cannot find package 'some-package' imported from /app/index.jsError [ERR_MODULE_NOT_FOUND]: Cannot find module '/app/dist/utils' imported from /app/dist/index.js
Full Error Example
node:internal/modules/esm/resolve:838
throw new ERR_MODULE_NOT_FOUND(packageName, fileURLToPath(base), null);
^
Error [ERR_MODULE_NOT_FOUND]: Cannot find module './utils' imported from /app/index.js
Did you mean to import ./utils.js?
at packageResolve (node:internal/modules/esm/resolve:838:9)
at moduleResolve (node:internal/modules/esm/resolve:907:20)
at defaultResolve (node:internal/modules/esm/resolve:1038:11)
at ModuleLoader.defaultResolve (node:internal/modules/esm/loader:557:12)
at ModuleLoader.resolve (node:internal/modules/esm/loader:530:38)
at ModuleLoader.getModuleJob (node:internal/modules/esm/loader:246:38)
at ModuleWrap.<anonymous> (node:internal/modules/esm/module_job:87:39) {
code: 'ERR_MODULE_NOT_FOUND',
url: 'file:///app/utils'
}
Since Node.js 18, the error message sometimes includes a hint: "Did you mean to import
./utils.js?" — a strong signal that a missing extension is the root cause.
The error object also carries a url field containing the unresolved specifier as a
file:// URL.
ERR_MODULE_NOT_FOUND vs MODULE_NOT_FOUND
| Property | ERR_MODULE_NOT_FOUND (ESM) | MODULE_NOT_FOUND (CJS) |
|---|---|---|
| Loader | Native ESM (import) |
CommonJS (require()) |
| Triggered by | import statements, import() |
require() calls |
| Message format | Cannot find module '...' imported from /path/to/file.js |
Cannot find module '...' + requireStack |
| Automatic extension resolution | No — specifier must be exact | Yes — tries .js, .json, .node |
| Automatic index.js lookup | No — directory imports are forbidden | Yes — require('./dir') tries ./dir/index.js |
| When it fires | Package has "type":"module" or file is .mjs |
File is .js without "type":"module" or is .cjs |
All Causes at a Glance
| Cause | Typical symptom | Quick fix |
|---|---|---|
Missing .js extension in relative import |
Cannot find module './utils' — file utils.js exists |
Change to import ... from './utils.js' |
TypeScript source — wrong moduleResolution |
Compiled output has bare imports; fails at runtime | Set module + moduleResolution: Node16 in tsconfig |
Missing "type":"module" in package.json |
ESM syntax in .js → CJS loader → different errors |
Add "type":"module" to package.json |
| Directory import (no index.js auto-resolution) | Cannot find module './models' — directory exists |
Import ./models/index.js explicitly |
| Package not installed | Cannot find package 'express' |
npm install express |
Missing package exports map entry |
Cannot find module 'pkg/internal' |
Use a public export path or createRequire |
| CJS-only package without ESM interop | Cannot find package 'legacy-pkg' |
Use createRequire(import.meta.url) |
| Case sensitivity on Linux/CI | Works on macOS/Windows, fails on Linux | Match filename case exactly in import path |
| ts-node without ESM mode | Fails with extensions at compile time or at runtime after tsc | Use tsx or configure ts-node ESM mode |
| Wrong relative path level | Cannot find module '../utils' when it's '../../utils' |
Verify path relative to current file, not project root |
Cause 1 – Missing .js Extension in Relative Import
This is the single most common cause. The ESM specification requires that relative specifiers
include the full file extension — there is no fallback resolution. Node.js will not try
./utils.js just because ./utils was requested.
CommonJS vs ESM resolution side by side
// CommonJS (require) — extension is optional
const utils = require('./utils'); // Node.js tries utils.js, utils.json, utils/index.js
const utils = require('./utils.js'); // also works
// ESM (import) — extension is MANDATORY for relative specifiers
import { helper } from './utils'; // ERR_MODULE_NOT_FOUND: Cannot find module './utils'
import { helper } from './utils.js'; // correct
Common patterns that need updating
// Before (worked in CJS, fails in ESM)
import { formatDate } from './helpers/format';
import config from '../config';
import * as db from './db/index';
// After (ESM-compliant)
import { formatDate } from './helpers/format.js';
import config from '../config.js';
import * as db from './db/index.js';
import data from './data.json' with { type: 'json' };Without the
with { type: 'json' } assertion, Node.js throws
ERR_IMPORT_ASSERTION_TYPE_MISSING in some versions.
Cause 2 – TypeScript: Wrong moduleResolution Setting
This is the #1 TypeScript ESM pain point. TypeScript does not rewrite import paths
during compilation — whatever you write in the .ts source is emitted verbatim into the
compiled .js output. If you wrote import { x } from './utils' (no
extension), the compiled output also has no extension, and Node.js's ESM loader throws
ERR_MODULE_NOT_FOUND.
The three moduleResolution options and what they mean for ESM
| moduleResolution | Enforces .js extension? | Use with | Notes |
|---|---|---|---|
node (legacy) |
No — TypeScript won't warn about missing extensions | CJS projects only | Compiled output fails at runtime in ESM mode |
node16 / nodenext |
Yes — TypeScript errors at compile time if extension is missing | Node.js ESM (type: module) |
Mirrors actual Node.js ESM resolution rules exactly |
bundler |
No — extensions are optional | Vite, esbuild, webpack, tsup | Bundler rewrites paths; do not use for Node.js ESM without a bundler |
Correct tsconfig.json for Node.js ESM
// tsconfig.json — recommended for Node.js ESM projects
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16", // or "NodeNext"
"moduleResolution": "Node16", // or "NodeNext" — must match module
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
}
}
TypeScript source file — use .js extension even though source is .ts
// src/index.ts
// Import uses .js extension — TypeScript resolves this to src/utils.ts at compile time
// The compiled dist/index.js will contain: import { helper } from './utils.js'
// which correctly references dist/utils.js at runtime
import { helper } from './utils.js'; // correct — .js refers to the compiled output
import { helper } from './utils.ts'; // wrong — TypeScript rejects this
import { helper } from './utils'; // wrong — ERR_MODULE_NOT_FOUND at runtime
moduleResolution: "Node16", TypeScript will
show a compile-time error "Relative import paths need explicit file extensions..." if you
forget .js. This converts a runtime ERR_MODULE_NOT_FOUND into a
compile-time type error — catch it before shipping.
Correct tsconfig.json for bundler-based projects (Vite, esbuild, tsup)
// tsconfig.json — for projects where a bundler handles resolution
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler", // bundler handles extensions, not Node.js
"outDir": "./dist",
"strict": true
}
}
Cause 3 – Missing "type": "module" in package.json
Without "type": "module", Node.js treats .js files as CommonJS. An
import statement in a CJS file throws SyntaxError: Cannot use import statement
outside a module, not ERR_MODULE_NOT_FOUND. However, the missing field is
still a common root cause that puts the whole project on the wrong loader path.
// package.json — required for ESM
{
"name": "my-app",
"version": "1.0.0",
"type": "module", // ← enables ESM for all .js files in this package
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js"
}
}
Without "type": "module", use .mjs extension for individual ESM files
instead of .js. The .mjs extension always forces ESM regardless of the
type field.
// Option 1: use "type": "module" in package.json → all .js files are ESM
// Option 2: rename individual files to .mjs → those files are ESM
// Option 3: keep CJS and use dynamic import() for ESM packages
// Mixing .mjs and .cjs in one project
import { esm } from './helper.mjs'; // forced ESM
const cjs = require('./legacy.cjs'); // forced CJS (inside a .cjs file)
const esmPkg = await import('./utils.mjs'); // dynamic import from CJS file
Cause 4 – Directory Import (No Automatic index.js in ESM)
In CommonJS, require('./models') automatically resolves to ./models/index.js
if models is a directory. The ESM loader does not do this —
ERR_UNSUPPORTED_DIR_IMPORT
is thrown instead (or ERR_MODULE_NOT_FOUND if the directory itself doesn't exist).
// Wrong — ESM does not resolve directories to index.js
import * as models from './models';
import * as models from './models/';
// Correct — explicit file path
import * as models from './models/index.js';
// Also correct — add a package.json inside the directory
// models/package.json:
// { "type": "module", "exports": { ".": "./index.js" } }
// Then: import * as models from './models'; works via exports map
Cause 5 – Package Not Installed
When importing a third-party package and the package is not installed, the ESM loader throws
Cannot find package 'package-name'. This behaves identically to the CJS case.
# The error
Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'express' imported from /app/index.js
# Fix
npm install express
# or
yarn add express
# or
pnpm add express
// After installing, the ESM import works normally
import express from 'express';
import { Router } from 'express';
./ prefix) by looking in node_modules. For bare package specifiers
in ESM, no file extension is required — the extension requirement only applies
to relative (./, ../) and absolute (/) specifiers.
import express from 'express' is always valid once the package is installed.
Cause 6 – Missing or Incomplete Package exports Map
Modern packages use the "exports" field in package.json to declare
which subpaths can be imported. If you try to import a path not listed in exports,
the ESM loader throws ERR_MODULE_NOT_FOUND (or
ERR_PACKAGE_PATH_NOT_EXPORTED
if the package has an exports map but the specific path is missing).
// Error: Cannot find package 'some-pkg/internal/utils' imported from /app/index.js
// because package.json exports does not include that path
// package.json (in node_modules/some-pkg)
{
"exports": {
".": "./dist/index.js",
"./helpers": "./dist/helpers.js"
// "./internal/utils" is NOT listed — importing it throws ERR_MODULE_NOT_FOUND
}
}
// Fix option 1: use a public export path
import { helper } from 'some-pkg/helpers';
// Fix option 2: if you control the package, add the export
{
"exports": {
".": "./dist/index.js",
"./helpers": "./dist/helpers.js",
"./internal/utils": "./dist/internal/utils.js" // added
}
}
// Fix option 3: use createRequire to bypass the exports map (CJS packages only)
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const utils = require('some-pkg/internal/utils');
Cause 7 – CJS-only Package Imported in ESM Context
Some older or niche packages are CommonJS-only and have no ESM export in their
package.json. Node.js's ESM loader can still load them — CJS packages
are interoperable with ESM via the default export. If the error fires, the package
is likely not installed, or its "main" field points to a non-existent file.
// CJS packages CAN be imported in ESM — no special handling needed in most cases
import lodash from 'lodash'; // works — lodash is CJS
import { merge } from 'lodash'; // also works (named imports from CJS default export)
// If the package has no ESM build AND its main field is broken, use createRequire
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const legacyPkg = require('legacy-cjs-only-package');
const { specificExport } = require('another-cjs-package/lib/specific');
Cause 8 – TypeScript with ts-node (CJS Hook Active)
By default, ts-node registers a CommonJS hook. When your project has
"type": "module", ts-node may fail or fall back in unexpected ways.
The compiled or interpreted output then runs through the ESM loader with bare specifiers that were
fine under the CJS hook but fail under strict ESM.
Option A — Use tsx (recommended for development)
# Install tsx
npm install --save-dev tsx
# Run TypeScript ESM files directly — no tsconfig tweaks needed
npx tsx src/index.ts
# Or add to package.json scripts
{
"scripts": {
"dev": "tsx src/index.ts",
"dev:watch": "tsx watch src/index.ts"
}
}
Option B — Use node --experimental-strip-types (Node.js 22.6+)
# Node.js 22.6+ can strip TypeScript types natively (no compilation step)
node --experimental-strip-types src/index.ts
# Node.js 23.6+ — no flag needed
node src/index.ts
Option C — Configure ts-node for ESM mode
// tsconfig.json — ts-node ESM configuration
{
"compilerOptions": {
"module": "Node16",
"moduleResolution": "Node16",
"target": "ES2022"
},
"ts-node": {
"esm": true,
"experimentalSpecifierResolution": "node" // only if needed for legacy imports
}
}
// Run with:
// node --loader ts-node/esm src/index.ts
// or:
// ts-node-esm src/index.ts
Option D — Compile with tsc and run compiled output
# Build TypeScript to JavaScript
npx tsc
# Run the compiled output (Node.js reads dist/index.js with .js extensions intact)
node dist/index.js
Cause 9 – Case Sensitivity on Linux / CI
macOS (HFS+/APFS default) and Windows (NTFS default) are case-insensitive. Linux is
case-sensitive. An import of ./Utils.js when the file is utils.js
works locally but throws ERR_MODULE_NOT_FOUND in CI (usually Ubuntu) or in a
Docker container (usually Alpine/Debian Linux).
// File on disk: src/utils.js
// Wrong — works on macOS/Windows, throws ERR_MODULE_NOT_FOUND on Linux
import { helper } from './Utils.js';
import { helper } from './UTILS.js';
// Correct — matches the actual filename on disk exactly
import { helper } from './utils.js';
Use eslint-plugin-import with the import/no-unresolved rule (or
TypeScript's forceConsistentCasingInFileNames compiler option) to catch
case mismatches at lint/compile time before they reach CI.
Safe ESM Import Patterns
Relative imports — always use .js
// src/server.ts (TypeScript, compiled to dist/server.js)
import { createApp } from './app.js'; // compiled: dist/app.js
import { db } from './db/connection.js'; // compiled: dist/db/connection.js
import { UserSchema } from './schemas/user.js'; // compiled: dist/schemas/user.js
import type { Config } from './types.js'; // type-only import, erased at compile time
ESM entry point pattern with package.json exports
// package.json
{
"name": "my-library",
"version": "1.0.0",
"type": "module",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
},
"./helpers": {
"import": "./dist/helpers.js",
"types": "./dist/helpers.d.ts"
}
},
"main": "./dist/index.cjs",
"module": "./dist/index.js"
}
ESM + CJS hybrid package (dual build)
// tsup.config.ts — build both ESM and CJS from TypeScript source
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
format: ['cjs', 'esm'], // produce both .cjs and .js
dts: true, // generate .d.ts files
splitting: false,
sourcemap: true,
clean: true,
});
Using import.meta.url instead of __dirname in ESM
// In ESM, __dirname and __filename are not available
// Use import.meta.url instead
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Now use __dirname as you would in CJS
const configPath = join(__dirname, '../config/default.json');
// Or use URL directly — often cleaner in ESM
const configURL = new URL('../config/default.json', import.meta.url);
import config from '../config/default.json' with { type: 'json' };
Dynamic import() for conditional loading
// Dynamic import works in both CJS and ESM files
// Useful for loading ESM-only packages from CJS, or for lazy loading
async function loadModule(name) {
try {
const mod = await import(name);
return mod.default ?? mod;
} catch (err) {
if (err.code === 'ERR_MODULE_NOT_FOUND') {
console.error(`Module '${name}' not found. Run: npm install ${name}`);
process.exit(1);
}
throw err;
}
}
// Example: load chalk (ESM-only) from a CJS file
const chalk = await loadModule('chalk');
Debugging Checklist
- Read the "imported from /path/to/file.js" part of the message — it tells you exactly which file has the bad import. Open that file and find the import statement.
- Check if Node.js is hinting: "Did you mean to import ./utils.js?" — if yes, add the
.jsextension immediately. - Confirm the file actually exists on disk with the exact casing. On Linux/CI, case matters.
- Check
package.jsonfor"type": "module"— without it,.jsfiles run as CJS. - For TypeScript projects: check
tsconfig.json— ismoduleandmoduleResolutionset toNode16orNodeNext? - For TypeScript: is the compiled output in
dist/missing the.jsextension in imports? If so, the tsconfig settings are wrong. - Is it a package (not a relative file)? Run
npm ls <package-name>to confirm it's installed locally. - Is it a subpath import like
pkg/lib/utils? Check if that path is listed in the package's"exports"field. - Is it a directory import (
./modelspointing to a folder)? Rewrite as./models/index.js. - Run
node --input-type=module <<< "import './your-file.js'"to test ESM resolution in isolation.
Frequently Asked Questions
What is ERR_MODULE_NOT_FOUND in Node.js?
ERR_MODULE_NOT_FOUND is thrown exclusively by Node.js's native ESM
(import) loader when it cannot resolve a module specifier. It is the ESM
counterpart to the CommonJS MODULE_NOT_FOUND code. The full message reads:
Error [ERR_MODULE_NOT_FOUND]: Cannot find module './utils' imported from /app/index.js.
Unlike the CJS loader, the ESM loader requires exact specifiers — it never adds .js
or looks for /index.js automatically.
Why does ERR_MODULE_NOT_FOUND happen even when the file exists?
The file exists, but the import path is missing the .js extension. The ESM
loader looks for a file literally named utils (no extension) and finds nothing.
Change import { x } from './utils' to import { x } from './utils.js'.
This is the #1 cause of this error.
How do I fix ERR_MODULE_NOT_FOUND in a TypeScript project?
Set "module": "Node16" and "moduleResolution": "Node16" in
tsconfig.json. Then add .js extensions to all relative imports in
your .ts source files:
// src/index.ts
import { helper } from './utils.js'; // .js = compiled output, not source
TypeScript will resolve ./utils.js to ./utils.ts at compile time.
The emitted dist/index.js retains ./utils.js, which correctly
references dist/utils.js at runtime.
Why does it work locally with ts-node but fail in production?
ts-node (without --esm) uses the CJS hook, which silently resolves
missing extensions. In production, Node.js runs the compiled .js output through
the strict ESM loader, which does not add extensions. The fix is to ensure all imports in
.ts source files have .js extensions so the compiled output is
correct, then compile with tsc and run the compiled output.
Alternatively, switch the dev runner to tsx which behaves the same in both
environments.
What is the difference between ERR_MODULE_NOT_FOUND and MODULE_NOT_FOUND?
MODULE_NOT_FOUND is thrown by require() (CommonJS loader).
ERR_MODULE_NOT_FOUND is thrown by import (ESM loader).
The ESM error message includes "imported from /path/to/file.js". The CJS error
includes a requireStack array. The key behavioral difference: the CJS loader
automatically tries .js, .json, /index.js; the ESM
loader does none of this.
How do I use a CJS-only package inside an ESM project?
Most CJS packages work fine with a regular ESM import — Node.js handles CJS/ESM
interop automatically. If you get ERR_MODULE_NOT_FOUND for a CJS package, it is
usually not installed or its "main" field points to a missing file. For accessing
internal CJS paths that bypass the exports map, use createRequire:
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const internal = require('legacy-pkg/lib/internal');
Why does importing a directory cause ERR_MODULE_NOT_FOUND in ESM?
The ESM loader does not resolve directories to index.js — that is a CJS-only
behavior. The proper ESM error for an existing directory is
ERR_UNSUPPORTED_DIR_IMPORT, but if the directory itself does not exist you get
ERR_MODULE_NOT_FOUND. Fix: always import the full file path including filename
and extension: import './models/index.js' instead of import './models'.
How do I fix ERR_MODULE_NOT_FOUND in Docker or CI?
If the error is for a package (bare specifier), node_modules was not installed
inside the container. Structure your Dockerfile correctly:
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
CMD ["node", "dist/index.js"]
If the error is for a relative file (./utils), the ESM extension rule applies
equally in Docker/CI — add .js to the import.
Should I use moduleResolution: bundler or nodenext for my TypeScript project?
Use moduleResolution: "NodeNext" (or "Node16") when your project
runs directly on Node.js — this mirrors actual Node.js ESM resolution and enforces
.js extensions at compile time, preventing runtime
ERR_MODULE_NOT_FOUND. Use moduleResolution: "bundler" when a
bundler (Vite, esbuild, tsup, webpack) processes your output — bundlers rewrite specifiers
and do not need explicit extensions. Never use moduleResolution: "node"
(legacy) for new ESM projects.
Related Errors
MODULE_NOT_FOUND– the CommonJSrequire()counterpart; thrown whenrequire()cannot find a module (extensions optional, index.js auto-resolved)ERR_UNSUPPORTED_DIR_IMPORT– thrown when an ESM import targets a directory that exists (as opposed to a path that doesn't exist at all)ERR_PACKAGE_PATH_NOT_EXPORTED– package is installed but the specific subpath is not in the exports mapERR_REQUIRE_ESM– opposite direction: usingrequire()on a file that is ESM-onlySyntaxError: Cannot use import statement outside a module– usingimportin a file that Node.js treats as CJS (missing"type":"module")ERR_UNKNOWN_FILE_EXTENSION– ESM loader encounters a.tsor other non-native extension it cannot executeERR_INVALID_PACKAGE_CONFIG– malformedpackage.jsoncausing module resolution to fail before the file lookup