Node 23 was released this week, and the hot ticket item probably is the fact that you can now require()
files that use ESM (import
/export
).
This is helpful because ESM and CommonJS (require
/module.exports
) are kind of different worlds and before this change if you wanted to use a “module” from your CommonJS file, you would need to do something like:
const theThing = await import('some/module/file.mjs');
This is called a dynamic import and it’s extra annoying, because you can’t just put this at the top of your file. It returns a Promise, so you can only import these modules ‘asynchronously’ or in functions. The reason is that you can only
await
inside functions in CommonJS files. So this syntax prevents importing a “module” and immediately using it. To use a module in CommonJS, you’ll need some kind of initialization logic.
Many Javascript packages resort to shipping both a ‘CommonJS’ and a ‘ESM’ version of their code to reduce this kind of annoyance, but not everyone does.
The Node 23 change
Node 23 makes it possible to load ESM modules transparently via require()
. This means that the above example can be rewritten to:
const theThing = require('some/module/file.mjs');
The important part here is not the usage of require
, but the absense of await
. This allows ESM modules to be loaded without this kind of initialization logic.
But there’s a big caveat:
This only works if the module you loaded in is not using top-level await. “Top level await” means awaiting things outside of (async) functions, which is possible in modules (but not CommonJS).
If a top-level await was used, a ERR_REQUIRE_ASYNC_MODULE
error will be thrown. But the important thing to note is that this doesn’t just apply to the file you are directly importing/requiring. It also applies to any files loaded by that file, at any level in either your
project or any dependency or sub-dependencies.
Using top-level await is now a BC break
Typically when we think of backwards compatibility breaks that require a major new version in semver, you might think of functions changing, or removing or arguments no longer being supported.
Before this change in Node, if your project was fully switched to ESM you might not think of placing a top-level await
anywhere in your code as a backwards compatibility break, but if it’s the first await
you might now
inadvertently break Node.js users, if they used require()
to bring in your module.
This means that the first top-level await
in your project (or any of your dependencies) might now constitute a new major version if you follow semver.
If you don’t want to do this, here are some other things you could do:
1. Tell your users you don’t support require()
You could effectively tell them if they use require()
they’re on their own. Chances are that your users won’t read this and do it anyway, but arguably
Truncated by Planet PHP, read more at the original (another 4607 bytes)