Node.js ESM "ERR_MODULE_NOT_FOUND" on Extensionless Imports
By Daniel Zoghalchali·Latchkey
Native ESM does not do CommonJS-style extension guessing. A relative import without a file extension that worked under CJS (or a bundler) fails under Node ESM with ERR_MODULE_NOT_FOUND.
What this error means
After switching to "type": "module" or running compiled ESM, imports like import { x } from "./util" fail with ERR_MODULE_NOT_FOUND, even though ./util.js exists. The same source ran fine under CommonJS or through a bundler.
Node output
node:internal/modules/esm/resolve:... Error [ERR_MODULE_NOT_FOUND]:
Cannot find module '/app/src/util' imported from /app/src/index.js
Did you mean to import './util.js'?
Diagnose it: what is different about the runner?
A build that passes locally and fails on a runner differs in a small number of predictable ways. Check those before changing build configuration, because the build config is usually not the thing that changed.
CommonJS and bundlers resolve ./util to ./util.js for you. Node’s native ESM resolver does not - it requires the full specifier including the .js (or .mjs) extension.
TypeScript output without rewritten extensions
TS source often imports ./util (no extension); when emitted as ESM, Node needs ./util.js. Without the right moduleResolution/rewrite, the emitted import is unresolvable.
How to fix it
Add explicit file extensions
Use the full extension in relative ESM imports.
index.mjs
// before
import { x } from './util';
// after
import { x } from './util.js';
Configure TypeScript for Node ESM
Use a Node-aware module resolution so TS expects/keeps the .js extension in emitted imports.
Case sensitivity. Linux runners are case sensitive, macOS is not. An import with the wrong case resolves locally and fails in CI.
Out of memory. Exit code 137 is a SIGKILL from the kernel, not a build error. Raise --max-old-space-size or use a larger runner.
devDependencies pruned.NODE_ENV=production makes npm ci skip devDependencies, so the build tool itself goes missing. Set it after install, not before.
How to prevent it
Always include file extensions in relative ESM imports.
Use nodenext module resolution for TypeScript targeting Node ESM.
Lint with a rule that enforces explicit extensions in ESM.
Frequently asked questions
What causes Node.js ESM "ERR_MODULE_NOT_FOUND" on extensionless imports?
There are 2 common causes: extensionless relative import under esm and typescript output without rewritten extensions. CommonJS and bundlers resolve ./util to ./util.js for you.
How do I fix Node.js ESM "ERR_MODULE_NOT_FOUND" on extensionless imports?
There are 2 fixes depending on which cause you have: add explicit file extensions and configure typescript for node esm. Work through them in order, since the first is the most common.
What does Node.js ESM "ERR_MODULE_NOT_FOUND" on extensionless imports actually mean?
After switching to "type": "module" or running compiled ESM, imports like import { x } from "./util" fail with ERR_MODULE_NOT_FOUND, even though ./util.js exists.
How do I stop Node.js ESM "ERR_MODULE_NOT_FOUND" on extensionless imports happening again?
Always include file extensions in relative ESM imports. The prevention section lists 3 changes that keep it from recurring.