Error [ERR_REQUIRE_ASYNC_MODULE]

require() cannot be used on an ESM graph with top-level await の直し方(Node.js の ERR_REQUIRE_ASYNC_MODULE)

FIX SUMMARY verified
Applies when
node:22require() of top-level await ESM

Verified: reproduced in node:22.12.0-alpine, then the ERR_REQUIRE_ASYNC_MODULE signature was gone after the fix (exit 0).

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.11require(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")にする選択肢もあります。

検証(machine-verified)

この修正は node:22 のバージョン固定コンテナ内で再現し、修正後に ERR_REQUIRE_ASYNC_MODULE のシグネチャが消えることを機械で確認しています。

verify — run-case.mjs
$ node run-case.mjs node/err-require-async-module
● reproduce ERR_REQUIRE_ASYNC_MODULE present ✓
● apply fix exit 0
● re-run ERR_REQUIRE_ASYNC_MODULE gone ✓
PASS verified · node:22.12.0-alpine · signature gone