Back to core workflows Fix dependency resolution Tune package metadata Jump to monorepo patterns

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 exports map exposes only an import condition (and a default that points at ESM), with no require condition 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.

Choosing a fix for ERR_REQUIRE_ESM A decision flow from a failing require call to four resolution paths depending on whether the caller can become ESM. require(esm-only-pkg) throws ERR_REQUIRE_ESM Can caller be ESM? change .js to import Yes: convert to ESM use static import statements yes No: stay CommonJS await import() bridge no or ask maintainer for a require condition
Pick the bridge by asking one question first: can the calling file become ESM?

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 engines and packageManager so require(esm) behavior is identical across machines and CI runners.
  • Add a smoke test that does require('your-pkg') in a CommonJS file and import('your-pkg') in an ESM file; run both in CI so a regression in your exports map fails the build.
  • Lint exports maps with a publint-style check in CI to catch a missing require condition before release.
  • Avoid mixing .js files without a "type" field — be explicit with .mjs/.cjs or set "type": "module" to remove ambiguity at the source.
  • Treat --experimental-require-module as 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