Node.js
Node.js — ERR_MODULE_NOT_FOUND after an ESM/CommonJS change
Written and reviewed by Sahil Srivastav
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/app/dist/config' imported from /app/dist/server.jsWhat this error actually means
Node’s ES module resolver could not resolve an import to a module it can load. The path and importing file in the error are the key evidence. If the importer lives under dist, the runtime is resolving the emitted application, not the TypeScript source tree or the path aliases understood by an editor.
Relative ESM specifiers generally need explicit file extensions and do not use CommonJS-style extension searching. A source import of ./config can therefore fail in Node even when a bundler accepted it. Package specifiers follow package resolution rules, including exports where present; a file existing somewhere in node_modules does not automatically make every subpath public.
Resolution failure is distinct from executing a module in the wrong format. require being unavailable in ESM, an unavailable named export, and a package export restriction have their own diagnostics. Current Node versions also differ in supported require/ESM interoperability, so changing every import to require is not a reliable diagnosis.
Causes, most common first
- 1The emitted relative import lacks the runtime extension. The source uses ./config while the emitted file is config.js. Type checking and bundling can resolve source paths differently from Node’s native ESM loader. Inspect the generated import rather than assuming successful compilation proves runtime resolution.
- 2A build artifact is absent or named differently. Configuration files, dynamically loaded modules or generated assets were not copied into the runtime image. Case mismatches become visible on Linux. Running from a different working directory also exposes code that builds dynamic paths relative to cwd rather than the module location.
- 3A runtime package was installed only as a development dependency. A local install includes tools and accidental transitive dependencies. A production install omitting devDependencies removes them. Every package imported by the running application should be declared in the correct runtime dependency set rather than borrowed from another package’s installation.
- 4The import targets an unsupported package entry point. An upgrade can change exports or build layout. Import the documented public path instead of reaching into a private dist directory. ERR_PACKAGE_PATH_NOT_EXPORTED specifically points to an exports restriction; preserve that distinction when choosing the repair.
When you see it
- The development runner works but node dist/server.js fails in production
- A Linux deployment fails on an import that worked on a case-insensitive filesystem
- The error appears after adding type: module or changing emitted file extensions
- A clean production dependency install fails while a developer’s node_modules works
How to diagnose it
Step 1
Read the exact importer and specifier
Start with the deployed error path. Inspect that emitted file and list neighbouring artifacts. An import from dist/server.js to ./config.js requires the corresponding runtime file relative to dist, regardless of where the source lived.
rg -n "import |from |require\(" dist/server.js
rg --files dist | sortStep 2
Record module format and runtime version
Inspect the nearest controlling package.json and the executable extension. .mjs and .cjs provide explicit format signals; .js interpretation depends on the package scope and runtime rules. Check the running image, not merely the repository configuration.
node --version
node -p "JSON.stringify(require('./package.json').type)"Step 3
Resolve a package from the runtime environment
Run this from the application’s deployed directory and substitute the actual package name. Resolution success checks an entry point, not every nested dependency or side effect, so follow it with the real entry-point smoke test.
node --input-type=module -e "console.log(import.meta.resolve('your-package'))"Step 4
Reproduce a clean production installation
Use a disposable checkout or build container to install from the lockfile with the production dependency policy and execute the emitted entry point. Do not delete a shared node_modules or overwrite another developer’s environment just to imitate deployment.
The fix
Make native ESM relative imports describe emitted runtime files, including their extensions. In a TypeScript Node-oriented project, align module and moduleResolution with the chosen Node mode and verify emitted paths. Do not append .ts specifiers blindly when the deployment actually runs compiled .js files.
Use module-relative URLs for colocated files, such as new URL("./schema.json", import.meta.url), then include those assets in the build output. Choose a parser or loader appropriate to the file type and installed runtime; path resolution alone does not make arbitrary assets executable modules.
Declare runtime packages directly and use their documented exports. If the package is CommonJS, use the supported interop form and inspect its actual exported shape. If a CommonJS consumer needs an ES module with asynchronous loading requirements, dynamic import can provide an explicit promise boundary.
Correct filename casing and enforce the same entry-point command in CI and production. Keep format migration separate from unrelated dependency upgrades where possible, so a path failure has a small, reviewable set of possible causes.
// package.json contains: { "type": "module" }
// Runtime layout: dist/server.js and dist/config.js
// dist/server.js
import { config } from "./config.js";
console.log(config.port);
// dist/config.js
export const config = { port: Number(process.env.PORT ?? 3000) };
// Resolve a colocated asset relative to the module, not process.cwd().
const schemaUrl = new URL("./schema.json", import.meta.url);How to stop it coming back
- Smoke-test the emitted entry point in a clean production-shaped environment
- Avoid depending on bundler-only aliases in code executed directly by Node
- Check filename casing and include required non-code assets in release artifacts
- Treat package exports as a public API rather than importing private build files
FAQ
Why does TypeScript compile an import Node cannot find?
The compiler’s resolution mode and a bundler or development loader may understand paths differently from native Node. Successful type checking does not prove that emitted specifiers point to deployed files. Inspect and execute the build output.
Should I remove type: module?
Only if the intended application format is CommonJS and the rest of the output matches it. Removing the field to fix one path can turn valid import syntax into another runtime error. Identify whether the failure concerns a missing file or the chosen module format.
Is ERR_REQUIRE_ESM the same problem?
No. It concerns a require/ESM loading compatibility boundary, while ERR_MODULE_NOT_FOUND concerns resolution. Supported interop changes across Node releases, so use the deployed version’s documentation and the exact error rather than treating every ESM failure alike.
Related
Other errors engineers hit next to this one
- READONLY You can’t write against a read only replica
- LOADING Redis is loading the dataset in memory
- CROSSSLOT Keys in request don’t hash to the same slot
- MOVED and ASK replies from Redis Cluster
- Redis clients stall during KEYS on a large keyspace
- A Redis lock lease expires while the worker still runs
- FATAL ERROR: Reached heap limit Allocation failed
- Unhandled promise rejection crashes the process