手元では動いていたコードを CI や本番の古い Node で走らせたら、fetch を呼んだ行で次のエラーが出て落ちることがあります。
ReferenceError: fetch is not defined
at Object.<anonymous> (/app/index.js:1:23)
コピペ検索用に表記ゆれを挙げておきます。いずれも同じ原因です。
ReferenceError: fetch is not definedfetch is not defined(ブラウザではなく Node 実行時に出たもの)
原因は、グローバルの fetch がフラグ無しの既定で使えるのが Node 18.0.0 からで、それより前の版では既定で fetch が定義されていないことです。ブラウザや新しい Node には最初からあるので、開発マシンでは通ります。CI や本番のベースイメージが Node 16 や 17 のままだと、その環境でだけ fetch is not defined になります。
これは典型的な版境界です。「手元では動くのに CI・古いコンテナでだけ落ちる」という食い違いは、ライブラリの入れ忘れではなく Node の版差が理由になっています。
いつから fetch が使えるのか
グローバルの fetch(Request / Response / Headers 含む)は、内部的に undici の実装を Node へ取り込む形で追加されました。フラグ無しの既定で使えるか、実験フラグが要るかは版によって分かれます。
| Node の版 | 既定の fetch | 実験フラグ(--experimental-fetch) |
|---|---|---|
| 16.14.x 以前 | 無し | フラグでも不可 |
| 16.15.0 〜 16.x | 無し | フラグを付ければ有効化できる |
| 17.0.0 〜 17.4.x | 無し | フラグでも不可 |
| 17.5.0 〜 17.x | 無し | フラグを付ければ有効化できる |
| 18.0.0 以降 | 既定で有効 | 不要 |
fetch の実装は 16.15.0 と 17.5.0 でフラグ付きで入りましたが、フラグ無しでそのまま使えるのは 18.0.0 からです。この記事のように何も付けずに node index.js する限り、17.x までは既定で落ちます。ドキュメント上で「実験的」の扱いが外れたのは 21.0.0 ですが、これは使えるか落ちるかの境界(18.0.0)とは別の話です。
再現(最小構成)
package.json の有無やモジュール形式に関係なく、fetch を参照した時点で落ちます。次の1ファイルを置きます。
// index.js
const res = await fetch('https://example.com');
console.log(res.status);
これを Node 16 で node index.js すると ReferenceError: fetch is not defined が出て、終了コードは 1 になります。ネットワークに到達する前、fetch という名前を解決できない段階で落ちるので、通信の可否とは無関係です。Node 18 以降なら fetch は定義済みなので、この ReferenceError は出ません。
解決
やりたいことによって直し方が分かれます。
1. Node を 18 以降に上げる
fetch を使い続けるなら、実行する Node(CI・本番の Docker ベースイメージを含む)を 18.0.0 以降(fetch がフラグ無しで入った版)に上げます。Dockerfile の FROM node:16 のような古い固定や、CI のバージョン設定(GitHub Actions の actions/setup-node の node-version など)を見直してください。本番では、その時点でサポート中の LTS に揃えるのが安全です(EOL を過ぎた版はセキュリティ修正が止まります)。メジャー版をまたぐときは、他の依存パッケージが新しい版に対応しているかを併せて確認してください。
2. ポリフィルを入れる(古い Node を残す場合)
古い Node をサポート対象に残すなら、fetch 相当を提供するパッケージを使います。Node の fetch の実体でもある undici や node-fetch が広く使われています。
// index.js(undici の fetch を使う)
import { fetch } from 'undici';
const res = await fetch('https://example.com');
console.log(res.status);
グローバルの fetch を前提に書かれた別のコードにも通したいときは、globalThis.fetch へ代入して橋渡しできます。
import { fetch } from 'undici';
if (typeof globalThis.fetch !== 'function') {
globalThis.fetch = fetch;
}
注意として、undici の新しめの版(v6 以降)は Node 18 以降を engines に要求します。つまり fetch が無くて困る主な対象=Node 16 以前では、最新の undici はインストールできません。Node 16 以前をポリフィルするなら、node-fetch(v2 系)を選ぶか、古い Node でも動く undici の旧版(v5 系)を明示的に指定してください。ここで入れる undici は npm パッケージで、Node 組み込みの fetch が内部に持つ undici とは別物です。
3. --experimental-fetch で有効化する(16.15〜17.x に固定せざるを得ないとき)
版を 16.15〜16.x か 17.5〜17.x に固定せざるを得ない事情があるなら、--experimental-fetch を付けて起動すると fetch が現れます。
node --experimental-fetch index.js
このフラグで有効化できるのは実装が入った 16.15.0 以降と 17.5.0 以降で、16.14 以前や 17.0〜17.4 では付けても現れません。いずれも一時しのぎで、これらの版はすべて EOL なので、恒久策としては 1(Node を上げる)か 2(ポリフィル)を選んでください。
切り分け(似ているが原因が違うもの)
fetch is not definedがブラウザ側で出た:この記事は Node 実行時の話です。ブラウザのfetchは別物で、主要ブラウザには存在します。スタックトレースに/app/index.jsのようなファイルパスやnode:internal/...が出ているなら Node 側の問題です。- 同じ版境界で別の API が落ちる:
fetchと同じく「新しい Node に組み込まれたグローバル」は、古い版でis not definedやis not a functionになります。たとえばstructuredClone(Node 17+)、crypto.randomUUID(14.17/16+)、AbortController(15+)、Blob(グローバルは 18+)などです。エラーの出方は違っても、原因は「その版にその API がまだ無い」で共通します。対処も同じで、Node を上げるかポリフィルを入れるかの二択になります。structuredCloneは ReferenceError: structuredClone is not defined の直し方、crypto.randomUUIDは TypeError: crypto.randomUUID is not a function の直し方 にまとめています。 fetchは動くがExperimentalWarningが出る:Node 18 の初期(18.0.0〜18.12.x)では、fetch()の初回呼び出し時にExperimentalWarning: The Fetch API is an experimental feature. This feature could change at any timeが標準エラー出力へ1度だけ出ます。この警告は 18.13.0 で削除されたので、それ以降の 18.x や Node 20 では出ません。ドキュメント上で「実験的」の扱いが外れた 21.0.0 とは別のタイミングです。警告を抑えたいだけなら--no-warnings(Node 20.11/21.3 以降なら種別を絞れる--disable-warning=ExperimentalWarning)で消せますが、--no-warningsはほかの警告も一緒に隠れる点に注意してください。- 組み込みモジュールを
node:付きで書いて落ちた:症状は違いますが「新しい書き方を古い Node が解釈できない」という同じ版境界です。Cannot find module 'node:fs'のパターンは node: プレフィックスが古い Node で通らないときの直し方 を参照してください。