Skip to content
Latchkey

Node.js ESM "ERR_MODULE_NOT_FOUND" on Extensionless Imports

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'?

Common causes

Extensionless relative import under ESM

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.

tsconfig.json
// tsconfig.json
{
  "compilerOptions": {
    "module": "nodenext",
    "moduleResolution": "nodenext"
  }
}

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.

Related guides

Tired of flaky CI? Latchkey auto-heals failed jobs and retries them for you. Start free →