Fixing ERR_REQUIRE_ESM in Node.js
ERR_REQUIRE_ESM is the runtime wall you hit when a CommonJS module reaches for an ESM-only package with require(). The fix is never to "force" the load — it is to choose a deliberate bridge between the two module systems. This page walks through the exact error, why Node.js refuses the call, and five concrete resolutions ranked from least to most invasive.
Exact Symptom
Node.js aborts module loading and prints a stack trace anchored on require():
Error [ERR_REQUIRE_ESM]: require() of ES Module /app/node_modules/some-pkg/index.js
from /app/server.js not supported.
Instead change the require of index.js in /app/server.js to a dynamic import()
which is available in all CommonJS modules.
at Object. (/app/server.js:3:18)
at Module._compile (node:internal/modules/cjs/loader:1254:14)
The message sometimes mentions a specific file inside node_modules rather than the package root, because the failing require is resolving through the package's exports map or main field down to a .mjs or "type": "module" file.
Root Cause Analysis
require() is synchronous and CommonJS-only. ES modules are evaluated asynchronously (top-level await, live bindings, deferred linking), so Node.js historically cannot satisfy a synchronous require() against an ESM graph and throws ERR_REQUIRE_ESM instead of guessing.
The package you are loading is ESM-only in one of two ways:
- It declares
"type": "module"and its entry file is plain.js, so Node parses it as ESM. - Its
exportsmap exposes only animportcondition (and adefaultthat points at ESM), with norequirecondition pointing at a CommonJS build.
Either way, when your CommonJS file calls require('some-pkg'), the resolver lands on an ESM artifact and refuses. This is the resolution boundary described in ESM and CJS Interoperability: the format of the target file, not your intent, decides what the loader will do. The same machinery is governed by the exports and type fields covered in Understanding package.json Fields.
Resolution Steps
Work top-down. The first option that fits your constraints is the one to adopt — do not stack hacks.
1. Convert the calling file to ESM (preferred)
If you control the caller and nothing forces it to stay CommonJS, make it an ES module and use a static import. This removes the boundary entirely.
{
"type": "module"
}
// server.js (now parsed as ESM because of "type": "module")
import chalk from 'chalk';
console.log(chalk.green('loaded as ESM'));
If you cannot change the whole package type, rename the single file to .mjs and update its references. This is the cleanest path because both files then live in the same module system.
2. Use a dynamic import() from CommonJS
import() is an async expression that works inside any CommonJS module. It returns a promise resolving to the module namespace. Use it when the caller must remain CommonJS (for example, a file loaded synchronously by a legacy framework).
// loader.cjs — stays CommonJS, loads ESM lazily
async function getRenderer() {
const { render } = await import('esm-only-renderer');
return render;
}
module.exports = { getRenderer };
The trade-off is that everything downstream becomes async. You cannot turn the result back into a synchronous require(); design the surrounding code to await it.
3. Bridge with createRequire (only for CJS targets)
createRequire builds a require function relative to a module URL. It is the inverse case — it helps you load a CommonJS dependency from an ESM file, and it will not fix ERR_REQUIRE_ESM, because the target is still ESM. Reach for it only after you have converted the caller to ESM (step 1) and then discover a different dependency is CommonJS-only:
// app.mjs
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const legacyCjs = require('old-cjs-only-pkg'); // fine: target is CJS
const { render } = await import('esm-only-renderer'); // ESM target
4. Ask the maintainer for a require condition
If the package ships a CommonJS build but forgot to expose it, the correct long-term fix is an exports map with a require condition. File an issue (or, for your own packages, patch it) so require() resolves to a .cjs artifact:
{
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs",
"default": "./dist/index.mjs"
}
}
}
Until upstream ships this, you can apply it locally with a patch tool, but treat that as temporary.
5. Enable require(esm) on Node.js 22+
Node.js 22 added the ability to require() an ES module that has no top-level await, behind a flag; it is unflagged on newer 22.x/23.x lines. When available, the synchronous require() succeeds and returns the module namespace.
# Node 22.x: opt in explicitly
node --experimental-require-module server.js
This is the lowest-friction fix where your runtime supports it, but it is not portable to Node 18/20, and it still fails if the ESM target uses top-level await. Do not rely on it for a published library that must run on older LTS lines.
Validation
Confirm the chosen fix actually resolves the module, in isolation:
# Reproduce against the package directly
node -e "require('esm-only-renderer')"
# Before fix: Error [ERR_REQUIRE_ESM]
# Validate the dynamic import bridge
node -e "import('esm-only-renderer').then(m => console.log(Object.keys(m)))"
# Inspect how Node resolves the package's conditions
node --conditions=require -e "console.log(require.resolve('esm-only-renderer'))"
For a published package, verify the require condition resolves to a real CommonJS file:
node -e "console.log(require('node:fs').existsSync(require.resolve('esm-only-renderer/package.json')))"
Prevention & CI Guardrails
- Pin a Node.js version with
enginesandpackageManagersorequire(esm)behavior is identical across machines and CI runners. - Add a smoke test that does
require('your-pkg')in a CommonJS file andimport('your-pkg')in an ESM file; run both in CI so a regression in yourexportsmap fails the build. - Lint
exportsmaps with a publint-style check in CI to catch a missingrequirecondition before release. - Avoid mixing
.jsfiles without a"type"field — be explicit with.mjs/.cjsor set"type": "module"to remove ambiguity at the source. - Treat
--experimental-require-moduleas a convenience for app code only; never assume it in library code that ships to consumers on Node 18/20.
Frequently Asked Questions
Why does Node throw ERR_REQUIRE_ESM instead of just loading the file?
Because require() is synchronous and ES modules evaluate asynchronously, Node cannot guarantee the module is fully linked at the moment require() returns. Rather than return a half-initialized object, it throws so you choose an explicit bridge.
Will createRequire fix ERR_REQUIRE_ESM?
No. createRequire produces a require function, but it still cannot synchronously load an ESM target — you will get the same error. It only helps you load CommonJS dependencies from inside ESM files.
Can I convert just one file to ESM instead of the whole package?
Yes. Rename that file to .mjs and use static import inside it. Node treats .mjs as ESM regardless of the package "type", so you avoid flipping the entire package.
Is --experimental-require-module safe for a library I publish?
Not as a dependency assumption. It is unavailable or flagged on Node 18 and 20, and it still fails when the ESM module uses top-level await. Ship a proper require condition instead so consumers do not need the flag.
Related
- ESM and CJS Interoperability — the resolution boundaries and conditional export rules behind this error.
- Understanding package.json Fields — how
type,exports, and conditions decide which artifact loads. - Resolving 'Named Export Not Found' in ESM — the sibling failure when you import a CommonJS package from ESM.
- How to Configure package.json for Dual Modules — shipping both a
requireandimportbuild to avoid this for consumers.