Node を新しめのバージョンに上げたあと、CommonJS から ESM を require() すると次のエラーで落ちることがあります。
Error [ERR_REQUIRE_ASYNC_MODULE]: require() cannot be used on an ESM graph with top-level await. Use import() instead.
メッセージ本文(require() cannot be used on an ESM graph with top-level await)やコード名 ERR_REQUIRE_ASYNC_MODULE で検索してたどり着く人が多いエラーです。
エラーメッセージが答えを半分言っています。require() を動的 import() に置き換えると直ります。ただし、なぜ「新しい Node に上げたら出た」のかを押さえておくと、同じ踏み方を避けられます。
このエラーは新しい Node でだけ出る
ERR_REQUIRE_ASYNC_MODULE は、ERR_REQUIRE_ESM を解消しようと Node を上げた人がよく踏みます。両者の関係はバージョンで切り替わります。
| Node のバージョン | CommonJS から ESM を require() したとき |
|---|---|
| 18.x/20.0〜20.18/22.0〜22.11 | require(esm) が既定で無効。ESM 全般が ERR_REQUIRE_ESM で落ちる |
| 20.19.0 以上/22.12.0 以上/23/24 | 通常の ESM は読める。ただし読み込む ESM がトップレベル await を含むと ERR_REQUIRE_ASYNC_MODULE で落ちる |
require(esm) の既定有効化は 20.19.0/22.12.0/23.0.0 からです。それより前(18.x、20.0〜20.18、22.0〜22.11)は require(esm) そのものが無効なので、トップレベル await の有無に関係なく ERR_REQUIRE_ESM になります。新しい Node で require(esm) が既定有効になった結果、**「普通の ESM は require() で読めるのに、トップレベル await を含む ESM だけ別のエラーで落ちる」**という状態が生まれました。つまり ERR_REQUIRE_ESM を消すために Node を上げた先で、条件を満たしたときだけ現れます。
再現(最小構成)
トップレベル await を含む ESM を 1 つ用意します。
// tla.mjs(トップレベル await を含む ESM)
export const value = await Promise.resolve('from an async ES module');
これを CommonJS から require() します。
// index.cjs
const mod = require('./tla.mjs');
console.log(mod.value);
Node 22.12 以上(require(esm) が既定有効な版)で実行すると ERR_REQUIRE_ASYNC_MODULE が出て、終了コードは 1 になります。export の値そのものではなく、モジュールの評価が非同期になることが原因なので、await を使っている行があるだけで該当します。
解決:require() を動的 import() に置き換える
ファイルは CommonJS のまま、require() の行だけを動的 import() に変えます。
// index.cjs(CJS のまま。require() を import() に置き換えただけ)
(async () => {
const mod = await import('./tla.mjs');
console.log(mod.value);
})();
これで終了コード 0 で正常に動きます。
仕組み:同期の require と非同期のトップレベル await
require() は同期的にモジュールを読み込み、その場で評価まで終える前提の仕組みです。一方、トップレベル await を含む ESM は評価の途中で待ちが入る、つまり評価が非同期になります。同期の require() は非同期評価の完了を待てないため、Node はこの組み合わせを実行時に拒否します。
require(esm) が既定有効になった新しい Node でも、拒否されるのはこの一点です。トップレベル await を含まない ESM なら評価は同期的に終わるので、require() でそのまま読めます。
動的 import() は Promise を返す非同期の読み込みなので、非同期評価を最後まで待てます。だから CommonJS のファイルからでも、トップレベル await を含む ESM を読み込めます。
切り分け(うまくいかないとき)
- 自分のコードに
awaitが無いのに出る:--experimental-print-required-tlaを付けて実行すると、require()の対象グラフのどこにトップレベルawaitがあるかを Node が出力します。require()した先の依存のどれかがトップレベルawaitを使っていることが多いので、その経路を動的import()に切り替えます。 - 古い Node(18 など)では違うエラーになる:18 系は
require(esm)自体が無効なので、トップレベルawaitの有無に関係なくERR_REQUIRE_ESMになります。そちらは require() of ES Module not supported(ERR_REQUIRE_ESM) を参照。 - 動的
import()に変えたら値の受け取り方で戸惑う:import()は Promise を返すので、const mod = await import(...)のようにawaitで受けます。トップレベルで使うなら、そのファイル自体を ESM(.mjsか"type": "module")にする選択肢もあります。