Node.js Error: ERR_REQUIRE_ESM — Cannot Require ESM
Error [ERR_REQUIRE_ESM]: require() of ES Module
/app/node_modules/node-fetch/src/index.js from /app/server.js not supported.
Instead change the require of /app/node_modules/node-fetch/src/index.js in
/app/server.js to a dynamic import() which is available in all CommonJS modules.
at Object.<anonymous> (/app/server.js:1:14) {
code: 'ERR_REQUIRE_ESM'
}
ERR_REQUIRE_ESM shows up the moment a package you depend on goes ESM-only and your code is still CommonJS. The Node CJS loader is synchronous; ESM is asynchronous; the two can’t be bridged via require(). Node throws rather than guess.
Three fixes, in priority order: migrate your code to ESM (cleanest), use dynamic import() (least invasive), or pin the package to its last CJS-compatible major (buys time). Almost every Node project hits this at least once during the 2022-2026 ecosystem-wide ESM migration; the boring fixes are well-trodden.
Why this happens
- Package went ESM-only in a major version bump. node-fetch v3, chalk v5, got v12, nanoid v4, p-limit v4, file-type v17, and many others ship pure ESM. Your package.json upgraded across the breaking version (e.g., `^2.6.0` → `^3.0.0`) and your CommonJS code can no longer require them.
- Mixed CJS/ESM project without correct package.json. Your project is CommonJS (no `"type"` field, defaults to CJS) but you installed an ESM-only dep. Or you set `"type": "module"` but your tooling (ts-node, jest, eslint configs) still loads code as CJS.
- TypeScript transpiles import to require. `import fetch from 'node-fetch'` looks like ESM in your `.ts` source, but if `tsconfig.json` has `"module": "commonjs"` (the default for older targets), tsc emits `require('node-fetch')` — which then hits ERR_REQUIRE_ESM at runtime.
- Bundler bundles for CJS but a dependency is ESM-only. Webpack, esbuild, or tsc emits CJS output. The bundle then `require()`s a transitive ESM-only dep at runtime. The bundler can't statically resolve ESM-from-CJS without dual-package handling.
- ts-node, jest, or eslint config running in CJS mode. Default ts-node uses CJS. Jest's transform emits CJS. ESLint's flat config loader is CJS. All of these hit ERR_REQUIRE_ESM the moment you reference an ESM-only package from configs they load.
How to fix it
Fixes are ordered by likelihood. Start with the first one that matches your context.
1. Convert your project to ESM
Cleanest long-term fix. Add `"type": "module"` to package.json, rename `.js` to `.mjs` (or keep `.js` if `type: module` is set), and replace `require()` with `import`. Watch for `__dirname`/`__filename`/`require.resolve` — they don't exist in ESM and need shims.
// Was: const fetch = require('node-fetch');
import fetch from 'node-fetch';
// ESM doesn't have __dirname / __filename — use:
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const res = await fetch('https://example.com');
console.log(await res.text());
2. Use dynamic import() inside async functions
Dynamic `import()` is available in CommonJS. It's async, so wrap usage in an async function or top-level await (Node 14.8+ in ESM, or experimental in CJS). This works without changing your project's module type.
// CommonJS file, but uses ESM-only node-fetch via dynamic import.
async function main() {
const { default: fetch } = await import('node-fetch');
const res = await fetch('https://example.com');
console.log(await res.text());
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
3. Pin the package to its last CJS-compatible version
For projects you don't want to migrate yet, pin the package to its last CJS major. node-fetch 2.x, chalk 4.x, got 11.x, nanoid 3.x all keep CJS support. Document why in a code comment so future maintainers don't auto-upgrade.
{
"dependencies": {
"node-fetch": "^2.7.0",
"chalk": "^4.1.2",
"got": "^11.8.6",
"nanoid": "^3.3.7"
}
}
4. Configure TypeScript / ts-node for ESM output
In tsconfig.json: set `"module": "NodeNext"` (or `"esnext"` with `"moduleResolution": "NodeNext"`) and add `"type": "module"` to your package.json. ts-node needs the ESM loader: `node --loader ts-node/esm`.
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"strict": true
}
}
5. Use createRequire to load the ESM module's CJS twin (when it exists)
Some packages ship dual builds — ESM main, CJS shim under a subpath. `createRequire` can reach those. This is a workaround, not a fix; prefer the ESM migration.
const { createRequire } = require('node:module');
const requireCjs = createRequire(import.meta.url);
const cjsTwin = requireCjs('some-pkg/cjs');
Detection and monitoring in production
ERR_REQUIRE_ESM is a startup error — it crashes your process before any traffic flows. CI catches it if you run the full app on every PR. For dependency upgrades, use `npm outdated` and read the changelog of every major bump; ESM-only is almost always called out. Renovate/Dependabot can be configured to pin majors and require manual review for the ones that go ESM.
Related errors
- postgresECONNREFUSEDYour application tried to open a TCP connection to Postgres and the OS rejected it — Postgres isn't listening on the host:port you specified, or a firewall blocked the connection.
- nodejsEADDRINUSEYour Node process tried to bind a TCP port (e.g., 3000) that another process is already listening on. The OS returns EADDRINUSE because two processes can't bind the same address+port. Often it's the previous instance of your own server that didn't shut down cleanly.
- nodejsheap_out_of_memoryV8's old-generation heap filled up and the garbage collector couldn't free enough space, so V8 aborts the process with a fatal allocation failure. Default heap is ~4GB on 64-bit; long-lived references (caches, listeners, closures, big arrays) prevent reclamation.
- nextjsmodule_not_foundThe Next.js build (webpack/Turbopack) tried to resolve an import path and couldn't find it. Either the package isn't installed, the relative path is wrong, a TypeScript path alias isn't mirrored in `tsconfig.json` and `next.config.js`, or the file's case differs between disk and import (Linux is case-sensitive, macOS isn't).
- pythonModuleNotFoundErrorThe Python interpreter walked `sys.path` and couldn't find the module you imported. Most common cause: you installed the package in a different environment (different venv, different Python version, system pip vs project pip) than the one running your code.
Frequently asked questions
Why did my code suddenly start throwing ERR_REQUIRE_ESM after npm install? +
Can I keep CommonJS and still use ESM-only packages? +
How do I know if a package is ESM-only? +
My TypeScript code uses `import` but I still get ERR_REQUIRE_ESM. Why? +
Does Node 20 / 22 fix ERR_REQUIRE_ESM? +
Will switching to ESM break my Jest tests? +
I see `Cannot use import statement outside a module` instead of ERR_REQUIRE_ESM. Are they the same? +
Should I use top-level await once I migrate to ESM? +
When to escalate to Node.js support
ERR_REQUIRE_ESM is a configuration/dependency issue, not a Node bug. Don't file upstream. If a package's ESM-only release broke something subtle (wrong export shape, missing CJS twin in a dual-package claim), file with the package — most maintainers respond fast because the issue is repeated by every CJS user upgrading.