Skip to content
Latchkey

Node ERR_MODULE_NOT_FOUND - Fix ESM Import Resolution in CI

Under native ESM, Node does not guess file extensions the way CommonJS did. An import without an explicit extension (or to a non-existent path) fails with ERR_MODULE_NOT_FOUND.

What this error means

A type: module package (or .mjs) crashes resolving a relative import with ERR_MODULE_NOT_FOUND. It frequently appears after compiling TypeScript to ESM, where emitted imports lack the .js extension.

Node output
node:internal/errors ... [ERR_MODULE_NOT_FOUND]:
Cannot find module '/app/dist/utils' imported from /app/dist/index.js
Did you mean to import ./utils.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.

.github/workflows/ci.yml
- run: |
    node --version && npm --version
    echo "NODE_ENV=$NODE_ENV  CI=$CI"
    nproc && free -h && df -h /
    ls -la node_modules/.bin | head

Common causes

Missing file extension in ESM imports

Native ESM requires the full specifier, including the extension. import x from "./utils" fails; ./utils.js is required.

TypeScript emitted extensionless imports

TS source written ./utils and the compiler did not rewrite it to ./utils.js, so the emitted ESM cannot be resolved by Node.

Wrong path or missing index resolution

ESM does not auto-resolve directory index.js the way CJS does unless explicitly configured; a bare directory import fails.

How to fix it

Use explicit extensions in ESM

Add the file extension to relative imports.

TypeScript / JS
// before
import { x } from './utils';
// after
import { x } from './utils.js';

Align module settings

  1. Set tsconfig module/moduleResolution to a Node ESM mode (e.g. NodeNext).
  2. Reference directory entry points explicitly (./dir/index.js).
  3. Verify dist output paths exist and match the import specifiers.

The three that account for most of them

  • 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

  • Use explicit .js extensions in ESM/TS-to-ESM imports.
  • Adopt NodeNext module resolution in tsconfig.
  • Test the built ESM output in CI, not just the TS source.

Frequently asked questions

What causes Node ERR_MODULE_NOT_FOUND?
There are 3 common causes: missing file extension in esm imports, typescript emitted extensionless imports, and wrong path or missing index resolution. Native ESM requires the full specifier, including the extension.
How do I fix Node ERR_MODULE_NOT_FOUND?
There are 2 fixes depending on which cause you have: use explicit extensions in esm and align module settings. Work through them in order, since the first is the most common.
What does Node ERR_MODULE_NOT_FOUND actually mean?
A type: module package (or .mjs) crashes resolving a relative import with ERR_MODULE_NOT_FOUND.
How do I stop Node ERR_MODULE_NOT_FOUND happening again?
Use explicit .js extensions in ESM/TS-to-ESM imports. The prevention section lists 3 changes that keep it from recurring.

Related guides

References

Not every red build is your code. Latchkey repairs the ones that are not, on the runner. Start free → 30-day trial · No credit card