状況
深夜2時。本番環境のバックエンドサービスがエラーを吐いている。ログを確認すると、こんな内容が目に飛び込んでくる:
TypeError: fetch is not a function
at callExternalAPI (/app/services/api.js:12:18)
at async processRequest (/app/handlers/request.js:34:5)
ローカルでは問題なく動いていた。同僚のマシンでも正常だ。しかも、今回はちょっとしたアップデートを push しただけ。いったい何が起きたのか?
端的に言うと、Node.js が fetch を組み込み関数として認識していない——現在の Node バージョンでは、そもそも組み込まれていないからだ。
なぜこのエラーが起きるのか
fetch はもともとブラウザの API だ。Node.js がネイティブ実装を同梱し始めたのは Node.js 18(2022年4月リリース)からで、それ以前はポリフィルなしに fetch() を呼び出すと、まさにこのエラーが発生していた。
よくある原因:
- 本番サーバーは Node.js 14 か 16 を使っているが、開発マシンには Node.js 20 が入っている
- ブラウザ向けチュートリアルのサンプルコードをそのまま Node.js プロジェクトに貼り付けた
- Docker のベースイメージが想定より古い Node バージョンを使っている
- Node.js 17 を使っているが、
--experimental-fetchフラグを有効にしていない
デバッグ:まず根本原因を特定する
いきなり修正に飛びつかないこと。エラーが発生している環境で、実際に動いている Node のバージョンを確認しよう:
node --version
Docker コンテナの中で確認する場合:
docker exec -it your_container node --version
v18 未満だったら、それが原因だ。次は自分の状況に合った修正方法を選ぼう。
修正1:Node.js 18 以上にアップグレードする(推奨)
ランタイムを自由に選べる環境なら、これが最もシンプルな解決策だ。追加の依存関係も、ポリフィルも、余計なコードも一切不要。
# nvm を使う場合
nvm install 18
nvm use 18
node --version # v18.x.x 以上と表示されるはず
Docker の場合は、ベースイメージを更新する:
# 変更前
FROM node:16-alpine
# 変更後
FROM node:18-alpine
リビルドして再デプロイすれば完了。fetch はグローバルで使えるので、import は不要だ。
動作確認:
node -e "fetch('https://jsonplaceholder.typicode.com/todos/1').then(r => r.json()).then(console.log)"
エラーではなく、ターミナルに JSON オブジェクトが出力されれば成功だ。
修正2:node-fetch を使う(古い Node から抜け出せない場合)
レガシープロジェクト、ベンダー制約、本番環境のフリーズ——Node のアップグレードが選択肢にないケースは確かにある。その場合は、ポリフィルとして node-fetch を追加しよう:
npm install node-fetch
コードでの使い方:
// CommonJS (require)
const fetch = require('node-fetch');
// ESM (import)
import fetch from 'node-fetch';
注意: node-fetch v3 以降は ESM 専用だ。プロジェクトが CommonJS(require())を使っているなら、v2 に固定しよう:
npm install node-fetch@2
API はブラウザのネイティブ fetch とほぼ同じなので、既存のコードはそのまま動くはずだ。
動作確認:
const fetch = require('node-fetch');
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then(res => res.json())
.then(data => console.log(data));
修正3:グローバルポリフィル(一度設定して全体に適用)
ファイルごとに fetch を import するのが面倒なら、アプリのエントリーポイントでグローバルに注入する方法がある:
// index.js または app.js の先頭に記述
const fetch = require('node-fetch');
global.fetch = fetch;
// これでアプリ全体で import なしに fetch が使えるようになる
これはブラウザの動作を模倣したものだ。すでに何十ものファイルで fetch を呼び出していて、一つ一つ修正したくない場合に特に有効だ。
修正4:axios に切り替える(fetch の悩みをまるごと解消)
fetch にこだわりがないなら、axios という選択肢がある。v10 以降のすべての Node バージョンで動作し、JSON の処理も自動でやってくれるうえ、エラーメッセージも最初からわかりやすい:
npm install axios
const axios = require('axios');
const response = await axios.get('https://api.example.com/data');
console.log(response.data);
移行は簡単で、fetch(url).then(r => r.json()) を axios.get(url).then(r => r.data) に置き換えるだけ。たいていの場合、差分はそれだけで済む。
修正5:Node.js 17 — 実験的 fetch を有効にする
Node 17 は特殊なケースだ。fetch は存在するが、フラグで隠されている:
node --experimental-fetch your-script.js
package.json で常時有効にするには:
{
"scripts": {
"start": "node --experimental-fetch index.js"
}
}
これはあくまで一時的な回避策であり、根本的な解決策ではない。Node 17 は2022年6月にサポートが終了しているので、素直に18以上へアップグレードしよう。
環境の不一致という落とし穴
このバグには厄介なパターンがある——手元では問題なく動くのに、CI や本番環境に上げた瞬間に壊れるというケースだ。これはほぼ確実に、環境間での Node バージョンのズレが原因だ。
Node のバージョンを明示的に固定しよう。.nvmrc ファイルを追加する:
echo "18" > .nvmrc
または package.json に engines フィールドを設定する:
{
"engines": {
"node": ">=18.0.0"
}
}
GitHub Actions の場合は、このファイルを参照するよう setup-node を設定する:
- uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
こうすれば、ローカル・CI・本番環境がすべて同じバージョンで動くようになる。深夜のサプライズはもうおしまいだ。
まとめ
- Node のバージョンを固定すること。
.nvmrc、package.json のenginesフィールド、明示的な Docker イメージタグを使おう——node:latestは絶対に使わない。 - ブラウザの API は Node にデフォルトで存在しない。
fetch、localStorage、window——これらは自分で追加しない限り、Node 環境では使えない。 - Docker のベースイメージに注意する。 バージョンタグなしの
node:alpineは、イメージを pull するタイミングによって異なる Node バージョンを引っ張ってくることがある。 - 環境を揃えること。 本番が Node 16 で動いているなら、ローカルの開発環境も 16 にすべきだ——20 ではなく。

