The error, verbatim
Jump to the fix ↓node:internal/modules/esm/resolve:275
throw new ERR_MODULE_NOT_FOUND(
^
Error [ERR_MODULE_NOT_FOUND]: Cannot find module 'C:\project\src\utils' imported from C:\project\src\app.js
at finalizeResolution (node:internal/modules/esm/resolve:275:11)
at moduleResolve (node:internal/modules/esm/resolve:865:10)
at defaultResolve (node:internal/modules/esm/resolve:991:11) {
code: 'ERR_MODULE_NOT_FOUND'
}
Tested on
- Node
- 24.14.0 and 20.19.4
- Module type
- "type": "module"
- TypeScript
- 5.9.3, module nodenext
- OS
- Windows 11 Pro
Contents
The fix
Write the file extension in every relative import:
import { add } from './utils.js'; // not './utils'In TypeScript you still write .js, even though the file on disk is utils.ts. The extension you write is the one that exists after compiling:
import { hi } from './greet.js'; // greet.ts on diskImporting a folder is a different error with the same cause, ERR_UNSUPPORTED_DIR_IMPORT. Point at the file:
import { mul } from './lib/index.js'; // not './lib'Why it happens
CommonJS require() guessed for you. Given ./utils it tried utils.js, then utils.json, then utils/index.js. ES modules don’t guess: the import string is resolved as a URL, so ./utils means a file literally named utils, and when there’s none, resolution stops.
That’s why the message quotes a path with no extension and points inside node:internal/modules/esm/resolve rather than at anything you wrote. The file exists; the name you asked for doesn’t.
The TypeScript path-alias version
This one costs people the most time, because the code compiles cleanly and fails only when it runs.
{ "compilerOptions": { "paths": { "@/*": ["src/*"] } } }
import { hi } from '@/greet.js';
tsc accepts it. But paths only teaches the type checker where to look; it does not rewrite anything. The compiled file still says:
import { hi } from '@/greet.js';
and Node has never heard of @/:
Error [ERR_MODULE_NOT_FOUND]: Cannot find module 'C:\project\dist\greet' imported from C:\project\dist\main.js
Two ways out, both verified on this project:
1. Node’s own aliases, which exist at runtime. Add an imports map to package.json (the keys must start with #):
{
"imports": { "#lib/*": "./dist/*" },
"type": "module"
}import { hi } from '#lib/greet.js';2. Keep @/ and run through a resolver that understands it. tsx reads paths from tsconfig.json:
npx tsx src/main.tsWhat didn’t work
If you can’t face adding extensions everywhere
TypeScript will tell you exactly where they’re missing, with the corrected path, if you set "module": "nodenext":
error TS2835: Relative import paths need explicit file extensions in ECMAScript
imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './greet.js'?
That turns a runtime crash into a compile-time list you can work through once.
Run it yourself
The project I used is on GitHub, so you can trigger the error on your own machine in under a minute:
git clone https://github.com/nils44344/node-ts-error-repros
cd node-ts-error-repros/02-err-module-not-found
npm install && npm run repro
How this was tested
A small ES module project ("type": "module") with a relative import, a folder import and a TypeScript build using paths, run on Node 24.14.0 and Node 20.19.4 with TypeScript 5.9.3. Every error and every fix above is the real terminal output from that project.
— N.K., end of entry No.014