Skip to content
fixerror.dev
Node.js config

Node.js Error: ERR_REQUIRE_ESM — Cannot Require ESM

stderr text
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'
}
Node tells you the offending file (`/app/server.js`) and the ESM module it tried to require. Both are actionable.

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.

server.mjs javascript
// 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.

server.js javascript
// 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.

package.json json
{
  "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`.

tsconfig.json json
{
  "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.

shim.js javascript
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

Frequently asked questions

Why did my code suddenly start throwing ERR_REQUIRE_ESM after npm install? +
A dependency you have shipped a major version that's ESM-only — most commonly `node-fetch`, `chalk`, `got`, `p-queue`, or `nanoid`. Run `npm ls <pkg>` to see what version got installed and check its changelog. If you didn't intend to upgrade, restore the old major in package.json and run `npm install` again.
Can I keep CommonJS and still use ESM-only packages? +
Yes — use dynamic `import()` inside async functions. CommonJS supports `await import('esm-only')`; you just can't use the synchronous `require()`. The downside is the rest of your code has to handle the async load (no top-level imports for that package). For one or two ESM packages, it's pragmatic; for many, migrate to ESM.
How do I know if a package is ESM-only? +
Open `node_modules/<pkg>/package.json` and look at three fields: `"type": "module"` (ESM by default), `"main"` (extension `.mjs` or no `"main"`/`"exports"` for CJS), and `"exports"` (if it only declares `"import"` and no `"require"` for the entry, it's ESM-only). Most packages document the move in their README.
My TypeScript code uses `import` but I still get ERR_REQUIRE_ESM. Why? +
Because tsc transpiles `import` to `require` when `tsconfig.json` has `"module": "commonjs"`. Your source looks like ESM but the compiled output is CJS. Switch tsconfig `module` to `NodeNext` (and add `"type": "module"` to package.json) so tsc emits real ESM.
Does Node 20 / 22 fix ERR_REQUIRE_ESM? +
Node 22+ has an experimental `require(esm)` flag (`--experimental-require-module`) that lets CJS synchronously require some ESM modules. It's still gated and has restrictions (no top-level await, no live bindings). Don't depend on it in production yet. The boring, reliable path is ESM migration or dynamic import.
Will switching to ESM break my Jest tests? +
It can — Jest's default transform emits CJS. Either configure Jest to use `babel-jest` with the right preset, switch to Vitest (native ESM), or run Jest with the experimental ESM flag (`NODE_OPTIONS=--experimental-vm-modules`). Most teams switch to Vitest for ESM projects.
I see `Cannot use import statement outside a module` instead of ERR_REQUIRE_ESM. Are they the same? +
Related but different. `Cannot use import statement outside a module` is Node refusing to parse `import` syntax in a file it loaded as CJS — fix by setting `"type": "module"` or renaming to `.mjs`. ERR_REQUIRE_ESM is a runtime error from `require()` on an ESM file. The first is a parser error; the second is a loader error.
Should I use top-level await once I migrate to ESM? +
It works in ESM modules in Node 14.8+ and is convenient — but be careful in libraries you publish. Top-level await delays the module's initialization, which can deadlock circular imports and complicates bundlers. In application code (entry points, route handlers), it's fine; in shared libraries, prefer wrapping in an async init function.

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.