Node 23 Update: Potential Future Backward Compatibility Issue with Top-Level Await in Node.js

Node 23 Update: Potential Future Backward Compatibility Issue with Top-Level Await in Node.js

This week, Node 23 was released, and one of the most significant updates is the ability to use require() for files that utilize ESM (ECMAScript modules).

Previously, working with ESM and CommonJS (the traditional require()/module.exports syntax) felt like dealing with two different worlds. To bridge the gap, you often had to resort to dynamic imports like this:

const theThing = await import('some/module/file.mjs');        

The problem with dynamic imports is that they return Promises, meaning you can only use await within functions in CommonJS files. It complicates things, preventing you from importing and using a module immediately.

Instead, wrapping your module calls in some initialization logic would be best. Many JavaScript packages address this issue by shipping a CommonJS and an ESM version of their code. However, not all maintainers do this, leading to awkward workarounds.

The Node 23 Update

The new Node 23 release simplifies this by allowing ESM modules to be loaded directly via require() without dynamic imports. So now, you can do this:

const theThing = require('some/module/file.mjs');        

This change is notable because it uses require() and eliminates the need for await in the import. It allows for a smoother experience when loading ESM modules without additional logic.

However, there’s an important caveat: this only works if the ESM module you’re importing doesn’t use top-level await. Top-level await refers to awaiting things outside of async functions, which is allowed in ESM but not in CommonJS. If the module or any dependencies use top-level await, you’ll encounter an ERR_REQUIRE_ASYNC_MODULE error.

Backward Compatibility Concerns

Here’s where the backward compatibility (BC) issue comes into play. Previously, if your project had used ESM entirely, you wouldn’t have thought of a top-level await as something that could break things for Node.js users.

But now, if someone uses require() to load your module and contains a top-level await, you could accidentally break the code. The first instance of top-level await in your project could force a major version bump if you follow semver (semantic versioning).

Possible Workarounds

If you want to avoid this issue, here are some options:

  1. You Don’t Support require(). Agree that ‘require()’ for your module isn’t supported by your dev community. Of course, not everyone reads the documentation, but at least you’ve set expectations.
  2. Add a Dummy Await. If you think top-level await might eventually be added to your code or your dependencies, you could include a dummy await to “reserve” this functionality for the future:

await "Good things come to those that support await";        

3. Explicitly Break CommonJS Support

Another option is to use the export key in your package.json file to prevent CommonJS users from importing your package. You could include a CommonJS entry that throws an error, warning users to stick with ESM.

Option 2 is attractive because if Node.js changes its behaviour to support top-level await in CommonJS files fully, your package will automatically work without additional changes.

How Other Runtimes Handle This

? Bun allows using import and require() in the same file, reducing the trade-offs. However, using require() for a module with top-level await still has the same issue. Bun gives you fewer reasons to run into this problem.

? Deno doesn’t support CommonJS, so this issue doesn’t arise there.

Importing CommonJS into ESM

This issue only exists when importing ESM into CommonJS files. You can still import CommonJS files into ESM without encountering problems, as CommonJS doesn’t support asynchronous exports.

What Users Should Do

Although this issue primarily affects package maintainers, as a user, you can still encounter it, especially if some of the packages you rely on don’t handle it well. Here’s what you can do to protect yourself:

  1. Switch to ESM. Stop using CommonJS wherever possible. This problem only exists when mixing CommonJS and ESM. ESM is the future, and staying with CommonJS may eventually cause more issues.
  2. Test your codebase. Ensure your tests fully load your codebase to catch any potential dependency issues early.
  3. Identify ESM-only dependencies. Be cautious about using require() for ESM-only dependencies, be aware of sub-dependencies' risk, and introduce top-level awaits.

My Opinion

This update adds complexity to an already confusing JavaScript module landscape. We’ve gone from having two types of modules (CommonJS and ESM) to what now feels like three: 1. CommonJS, 2. ESM, 3. ESM without top-level awaits.

If any part of a module’s dependency tree switches to using top-level await, it can cause a cascade of compatibility issues for projects using require(). While the intent behind this change is good, I believe it creates more confusion and could lead to problems in the long run. I’d rather see CommonJS frozen at this point while focusing on making ESM more robust. But before all, ask the SANTEJS community.

要查看或添加评论,请登录

Michael W.的更多文章

  • AI: GREAT ANSWERS COME FROM GREAT QUESTIONS.

    AI: GREAT ANSWERS COME FROM GREAT QUESTIONS.

    Let's provoke creative thinking and set human-like guidelines. The heart of creative and critical thinking lies in…

  • The futureless app development: Exploring Low-Code Cross-Platform world.

    The futureless app development: Exploring Low-Code Cross-Platform world.

    Low-code cross-platform tools have emerged as a game-changer in the ever-evolving app development landscape. These…

    2 条评论
  • ONE-FRONT: Software Modelling

    ONE-FRONT: Software Modelling

    Defining a software modelling and delivery process provides a systematic approach, ensures consistency, and identifies…

  • Is "PWA" a new mobile APP?

    Is "PWA" a new mobile APP?

    The web has a superpower that native apps may never reach. Imagine that I can send you a link to a specific service on…

  • Dev-AI hybrid.

    Dev-AI hybrid.

    Hello, #chatgpt #openai enthusiasts! Following the hype and fear that I will remain jobless, I have decided to test the…

  • Quality Assurance: Unit Testing

    Quality Assurance: Unit Testing

    What is it? Unit testing is a set of functions that instantiates a small portion of an application and verifies its…

    1 条评论
  • Covid-19 app with OpenAI ChatGPT

    Covid-19 app with OpenAI ChatGPT

    Hello, #chatgpt #openai enthusiasts! Following the hype and fear that I will remain jobless, I have decided to test the…

  • Clean Code Across the Domains

    Clean Code Across the Domains

    Every line of code should be easily readable by others, and there are a few elements to achieve that: defining standard…

  • Is clean code a developer concern?

    Is clean code a developer concern?

    Coding standards are conventions designed to deliver high-quality code. Every line should be easily readable by others,…

  • Unit Testing.

    Unit Testing.

    What is it? A unit testing is a set of functions that instantiates a small portion of an application and verifies its…

    2 条评论

社区洞察

其他会员也浏览了