You upgraded a dependency — chalk, node-fetch, nanoid, one of the popular ones — and your CommonJS app fell over on startup: Error [ERR_REQUIRE_ESM]: require() of ES Module ... not supported. You didn’t change how you import it. The package maintainer went ESM-only in a major version, and your require() can’t load it anymore. The ERR_REQUIRE_ESM error has been a rite of passage since 2021, but the fix in 2026 is genuinely different from the advice you’ll find in older posts, because Node itself changed.
Quick fix
ERR_REQUIRE_ESM means you used require() to load a package that ships only as an ES module. Three fixes, best first: upgrade to Node 24 LTS (or a modern Node 22 LTS build), where require() can load a synchronous ESM package directly — you may still need a small .default tweak for default exports; or replace the require with a dynamic import() (it’s async, so it goes inside a function); or pin the dependency to its last CommonJS version while you migrate. The old “rewrite your whole app to ESM” is now the last resort, not the first.
// Old (throws on a now-ESM-only package):
const chalk = require('chalk');
// Dynamic import is async — it must live inside an async function, not top-level in CJS:
async function main() {
const { default: chalk } = await import('chalk');
console.log(chalk.green('loaded'));
}
main();
The error:
Error [ERR_REQUIRE_ESM]: require() of ES Module /app/node_modules/chalk/source/index.js
from /app/index.js not supported.
Instead change the require of index.js to a dynamic import() which is available in all CommonJS modules.
Why it happens
Packages have been migrating to ESM-only for years. When a maintainer publishes a major version with "type": "module" and no CommonJS build, your require('that-package') hits a module the CommonJS loader historically couldn’t synchronously evaluate, and Node throws ERR_REQUIRE_ESM. It’s the mirror image of Cannot use import statement outside a module: there you used import where Node expected CJS; here you used require where the package is ESM. (If Node can’t even resolve the package name before it gets to the loader, that’s a different problem — start with the Cannot find module fix first.)
What changed is Node. Support for require()-ing synchronous ES modules landed across the supported lines — Node 20.19+, Node 22.12/22.13+, and Node 23 — and it’s standard behaviour in Node 24 LTS, documented in the Node.js modules docs. On current supported Node versions, especially Node 24 LTS and modern Node 22 LTS builds, require() can load synchronous ESM packages by default. If you’re still on Node 18 or an old Node 20 build (both now end-of-life), upgrade rather than patch around the error.
The one constraint: top-level await
require(esm) works only if the target module and everything it imports are synchronous. If the ESM package uses top-level await, Node can’t load it synchronously and require() fails — on modern Node with a clearer error, ERR_REQUIRE_ASYNC_MODULE, instead of the old ERR_REQUIRE_ESM. There’s no way to make a synchronous call wait on an async module. In practice most libraries don’t use top-level await, so this covers the large majority of the packages that gave you trouble. For the ones that do, fall back to dynamic import().
One gotcha when require() does load an ESM module: a default export comes back on the .default property, not as the bare return value. This is exactly the chalk trap — you expect chalk.green and get a namespace object:
const chalk = require('chalk').default; // note .default
Fix 1: upgrade Node (the cleanest path)
If you’re on an end-of-life Node 18 or an old Node 20, moving to Node 24 LTS is the cleanest fix for most ERR_REQUIRE_ESM cases, because the runtime can now require() synchronous ESM. Check your version and upgrade:
node --version # v18.x or an old v20.x -> this is likely your real problem
Upgrading Node may remove the ERR_REQUIRE_ESM crash for synchronous ESM packages outright. For default-export packages you may still need the small .default adjustment above — so it’s “upgrade, then maybe touch one line,” not always zero changes. It’s still the first thing to try, and a good reason to stay on current LTS generally, the same way the TypeScript setup guide assumes a modern Node baseline.
Still failing after upgrading Node? Check this
If you upgraded and require() still throws, work through these — they’re where the long tail of “Node 22 require ESM still failing” reports actually live:
- Confirm the Node that runs the app, not just your shell.
node --versionlocally and inside Docker, CI, and your host’s runtime — the production runtime is often older than your laptop. - Verify the feature is on:
node -p "process.features.require_module"printstruewhenrequire(esm)is enabled. - Did the error change to
ERR_REQUIRE_ASYNC_MODULE? Then it’s no longer the old problem — the package uses top-level await, and you need dynamicimport()(Fix 2). - On Docker/Vercel/Railway/Render/GitHub Actions, pin the Node version explicitly (base image,
engines, or runtime setting) so prod matches local. - TypeScript project? Check
module,moduleResolution, andtype— your compiled output may still be emittingrequire()against an ESM target.
Fix 2: dynamic import()
Dynamic import() is available inside CommonJS and returns a promise, so it loads ESM (including modules with top-level await) from CJS code. The catch is that it’s async, so the calling code has to be able to await:
async function main() {
const { default: chalk } = await import('chalk');
console.log(chalk.green('loaded an ESM-only package from CommonJS'));
}
main();
For a one-off at startup this is clean. If a deep synchronous function needs the package, you’ll either lift the import to an async boundary or cache the module after a one-time async load. Dynamic import is the universal escape hatch — it works on every Node version and handles top-level await — at the cost of making that code path async.
Fix 3: pin the last CommonJS version
If upgrading Node isn’t an option right now and you can’t easily make the call site async, pin the dependency to its last version that shipped a CommonJS build:
npm install chalk@4 # chalk 5 went ESM-only; 4.x is CommonJS
This buys time, but it’s a holding action — you’re staying on an older major and missing its updates. Treat it as a deadline, not a destination, and plan the real fix (upgrade Node, or move the affected code to ESM).
When to just go ESM
If you keep hitting ERR_REQUIRE_ESM across many dependencies, that’s the ecosystem telling you where it’s going. Converting the project to "type": "module" — real import statements, file extensions on relative imports, __dirname replaced with import.meta.url — ends the whole class of error permanently. It’s more work than the three fixes above, so I wouldn’t do it just to silence one package, but if ESM-only deps are a recurring tax, migrating is the move that stops paying it.
Tested with: Node 24 LTS and Node 22 LTS, a CommonJS project, and ESM-only packages both with and without top-level await. Error reference: Node.js ERR_REQUIRE_ESM.
FAQ
What does ERR_REQUIRE_ESM mean?
It means you used require() to load a package that is published as an ES module only, with no CommonJS build. The CommonJS loader historically couldn’t synchronously evaluate an ESM file, so Node throws this error. It commonly appears after upgrading a dependency to a major version that went ESM-only.
How do I require an ESM-only package from CommonJS?
Easiest: upgrade to Node 24 LTS (or a modern Node 22 LTS build), where require() can load synchronous ESM directly — add .default for default exports. Otherwise replace require('pkg') with const { default: pkg } = await import('pkg') inside an async function, which works on any Node version. As a stopgap, pin the dependency to its last CommonJS version.
Why does ERR_REQUIRE_ESM work locally but fail in production?
Almost always a Node version mismatch. Your laptop runs a recent Node where require(esm) is enabled, but the Docker image, CI runner, or host runtime is on an older Node where it isn’t. Check node --version in the actual production environment and pin the Node version so prod matches local.
What is ERR_REQUIRE_ASYNC_MODULE?
It’s the error modern Node throws when you require() an ES module that uses top-level await. require() is synchronous and can’t wait on an async module, so instead of the older ERR_REQUIRE_ESM, Node names the real cause. The fix is dynamic import(), which is asynchronous and handles top-level await.
Why does require(esm) still fail with top-level await?
Because require() is synchronous and a module using top-level await is asynchronous — Node can’t block a synchronous call to wait for it. Node only allows require() of ESM when the module and its entire import graph are synchronous. For modules with top-level await, use dynamic import() instead.
Why do I get undefined when I require an ESM module’s default export?
When require() loads an ES module, the default export is on the .default property of the returned namespace object, not the return value itself. Use const pkg = require('pkg').default (or destructure const { default: pkg } = require('pkg')).
Should I convert my whole project to ESM to fix ERR_REQUIRE_ESM?
Not for a single package — upgrading Node or using dynamic import() is faster. But if ESM-only dependencies keep breaking your CommonJS app, converting to "type": "module" ends the entire class of error. Weigh the one-time migration cost against how often you’re hitting it.
