Skip to the solution
HardBackend7 min read

How does CommonJS require() an ES Module in modern Node.js?

1.1k views

The problem

Question presented to candidate: "An old CommonJS codebase needs to use a dependency that only ships ESM. What are the options in a current Node version?"

What a strong answer should cover:

  • Modern Node (from Node 22, with the capability broadening in later releases) lets require() load an ES Module synchronously and directly, as long as that module does not itself use top-level await.
  • This closed a long-standing, genuinely painful gap: for years the only way for CommonJS to consume an ESM-only package was an async import(), which cannot be used at the top of a synchronous require chain without restructuring the caller.
  • The one hard limitation is top-level await. require() is synchronous by contract, and a module suspended on an unresolved promise cannot return a value synchronously β€” Node throws a specific, named error rather than silently hanging or coercing.
  • The precise error is ERR_REQUIRE_ASYNC_MODULE, with a message pointing you at import() instead β€” a good thing to be able to name exactly, since it identifies a version-aware, non-obvious failure mode.
  • This does not turn a .mjs file into CommonJS. It is still an ES Module, evaluated as one; require() is simply now able to synchronously wait for that evaluation to finish, when nothing async blocks it.
  • The direction of travel matters for the interview: dynamic import() from CommonJS still works and is still the answer for anything using top-level await β€” this feature narrows the gap, it does not remove every case where you need it.

Clarifying questions expected:

  • "Does the target module use top-level await?" β€” that is the one thing that decides whether require() works at all.
  • "Which Node version is this running on?" β€” the capability rolled out and broadened across releases.

Code / implementation expected: Optional. Showing the successful case and the thrown error side by side is the clearest demonstration.

  • commonjs
  • esm
  • require
  • modules
  • interop

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

7 min read Β· 9 sections

Read the code

JavaScriptThe success case and the thrown error, side by side
// mod.mjs β€” plain ES Module, no top-level await
export const val = 42;

// From a CommonJS caller:
const m = require("./mod.mjs");
console.log(m.val);   // 42 β€” works directly, no import() needed


// tla.mjs β€” an ES Module WITH top-level await
await new Promise((r) => setTimeout(r, 10));
export const val = "from-tla";

// From CommonJS:
try {
  require("./tla.mjs");
} catch (e) {
  console.log(e.code, "-", e.message.split("\n")[0]);
}
// ERR_REQUIRE_ASYNC_MODULE - require() cannot be used on an ESM graph
// with top-level await. Use import() instead.

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 117 of 152 in Node.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Node.js questions
Up nextHard

Explain how memory management and Garbage Collection work in the V8 engine.

Continue