So you’ve got a stable Node.js project, you’re happy using require()
, and then you install a new package and get hit with:
Error [ERR_REQUIRE_ESM]: require() of ES Module not supported
If you’re like most devs, your first thought is: “Wait... do I have to rewrite my whole project?”
The short answer: not necessarily. Here's what’s going on, and how to handle it without tearing your codebase apart.
What’s the Issue?
Modern packages like lighthouse
, node-fetch
, and others are now shipping as ES Modules only. That means they require the use of import
/export
syntax instead of the older require()
and module.exports
.
The problem? Node doesn’t let you mix CommonJS and ESM unless you jump through some hoops. If your codebase is using CommonJS and you try to require()
a module that only supports ESM Node throws a tantrum. Instantly.
You might think switching everything over is simple, but…not really. Especially if your project is a few years old, or has a bunch of interconnected files. Even one outdated package can turn the whole thing into a game of dependency whack-a-mole.
Option 1: Convert Your Project to ESM
You can migrate your whole codebase to ESM. To do that, you add this to your package.json
:
"type": "module"
Then go file by file, updating all require()
and module.exports
statements into import
/export
. For example:
// Old
const fs = require('fs');
// New
import fs from 'fs';
It’s straightforward in theory, but depending on how big your project is, this can take hours, or days. And if you're using any tools, test runners, or libraries that still expect CommonJS, they might just break. So yeah...clean, but risky if you're mid-sprint or dealing with older dependencies.
Option 2: Use Dynamic import()
Now this one’s sneaky useful. If you only need to use one or two ESM-only libraries (like lighthouse
, for example), you don’t need to refactor the whole house. You can just dynamically import()
the module like this:
(async () => {
const lighthouse = await import('lighthouse');
// Use lighthouse here
})();
It works in CommonJS files and lets you sidestep the whole "type": "module"
thing. But yeah, the function has to be async
, which means if you're working inside a tight non-async setup, you'll have to juggle a bit. Still, this is probably the fastest fix with the least amount of regret.
Option 3: Use the CLI Instead (When Applicable)
If the library you're trying to use has a command line interface, you can just run it as a separate process. For example, lighthouse
has a great CLI. So you can call it like this from your CommonJS project:
const { exec } = require('child_process');
exec('lighthouse https://example.com --output=json', (err, stdout, stderr) => {
if (err) {
console.error('Error:', stderr);
return;
}
console.log('Lighthouse output saved.');
});
This is perfect for one-off utilities, background scripts, or internal tools. Downside? You're not really "using" the library programmatically, you're just shelling out to it. So you lose some flexibility. But honestly, if you're just looking to get results and move on, it's clean and reliable.
Final Thoughts
Node.js is moving toward ESM. That’s where the ecosystem is heading. But if you’re working in a legacy or mixed environment, you don’t have to fry your setup just to stay current.
Use import()
when you can, lean on the CLI when it makes sense, and only fully convert to ESM if you’re ready to commit to the shift.
The key here is don’t panic. The error message looks dramatic, but in most cases, there’s a clean workaround that doesn’t require rewriting your entire codebase in a caffeine-fueled weekend binge.
Walter Guevara is a Computer Scientist, software engineer, startup founder and previous mentor for a coding bootcamp. He has been creating software for the past 20 years.