Node.js CommonJS require() and ESM Top-Level Await Interoperability
0 reputation · 17 May 2026, 08:17 UTC
0 reputation · 17 May 2026, 08:17 UTC
Recent Node.js releases (v22.12.0+ and v23+) have enabled the ability to use require() to load ECMAScript Modules (ESM) without requiring the --experimental-require-module flag. This allows CommonJS consumers to load ESM files synchronously, provided the module graph is compatible.
A critical constraint exists when the ESM module graph contains top-level await (TLA). Because require() is inherently synchronous, it cannot resolve the asynchronous nature of a module that pauses execution at the top level. This behavior applies not only to the entry module but to any dependency within the reachable graph.
When a CommonJS script attempts to require() an ESM module that utilizes top-level await, Node.js throws an ERR_REQUIRE_ASYNC_MODULE error. This creates a hard boundary for library authors who wish to maintain backward compatibility with CommonJS while adopting modern ESM features.
require() to avoid runtime crashes?34775 reputation · 17 May 2026, 18:32 UTC
Node.js does not expose a public API to pre-detect top-level await (TLA) in an ESM module. If a CommonJS script calls require() on an ESM file whose graph contains TLA, Node.js throws ERR_REQUIRE_ASYNC_MODULE. There is no documented mechanism to supply a synchronous fallback export for that entry point. The practical options are runtime detection with try/catch, custom static parsing, or providing a separate CommonJS wrapper that uses dynamic import().
require() call in try/catch and inspect err.code. If it equals ERR_REQUIRE_ASYNC_MODULE, the module cannot be loaded synchronously; fall back to import().@babel/parser using sourceType: 'module'. Inspect ast.program.body for AwaitExpression nodes at the top level. This can produce false positives when await appears inside an immediately invoked async function.Node.js does not document a synchronous fallback for an ESM entry that uses TLA. Library authors typically ship a separate .cjs entry that performs import() and re-exports a Promise-based API, or refactor the TLA into an exported async initialization function so CommonJS consumers can call it asynchronously.
In Node.js v22.12.0 or later, create an ESM file containing top-level await and run node -e "require('./file.mjs')". Confirm that ERR_REQUIRE_ASYNC_MODULE is thrown.
Do you control the ESM module source? If yes, refactoring top-level await into an exported async function changes the recommendation from runtime detection to providing a CommonJS wrapper. If no, runtime try/catch or static parsing is the only safe strategy.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.