In the future using top-level await might be cause a backwards compatibility break in Node – Evert Pot

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)