タイムアウトやキャンセル処理で AbortController を使ったら、古い Node で次のエラーが出て落ちることがあります。
ReferenceError: AbortController is not defined
at Object.<anonymous> (/app/index.js:1:20)
コピペ検索用の表記ゆれです。いずれも同じ原因です。
ReferenceError: AbortController is not definedAbortController is not defined
原因は、グローバルの AbortController がフラグ無しの既定で使えるのが Node 15.0.0 からで、それより前の版では既定で AbortController が定義されていないことです。ブラウザや新しい Node には最初からあるので、開発マシンでは通ります。CI や本番のベースイメージが Node 14 のままだと、その環境でだけ AbortController is not defined になります。
これは典型的な版境界ですが、AbortController には見落としやすい点があります。公式ドキュメントは追加を Added in: v15.0.0, v14.17.0 と書いており、実装自体は 14.17.0 にバックポートされています。ただし 14 系では --experimental-abortcontroller フラグを付けたときだけグローバルに現れ、フラグ無しの既定ではどのパッチ(14.18・14.21 など)でも undefined のままです。だから「Node 14 のパッチを上げれば(何もせず)直る」ではなく、既定で使うなら 15 以降へ上げる必要があります。ここを取り違えると、14 系の中でパッチを上げて堂々巡りになります。
いつから AbortController が使えるのか
グローバルの AbortController(と対になる AbortSignal)は Node 15.0.0 でフラグ無しの既定になり、15.4.0 で安定扱いになりました。14.17.0 にも実験的に入っていますが、露出には起動フラグが要ります。
| Node の版 | 既定の AbortController | 実験フラグ(--experimental-abortcontroller) |
|---|---|---|
| 14.16.x 以前 | 無し | フラグでも不可 |
| 14.17.0 〜 14.x | 無し | フラグを付ければ有効化できる |
| 15.0.0 〜 15.3.x | 既定で有効(実験的) | 不要 |
| 15.4.0 以降 | 既定で有効(安定) | 不要 |
| 16 以降 | 有り | 不要 |
ドキュメントの Added in: v14.17.0 を見て「14 にあるはず」と思っても、フラグ無しでは現れないのがこの問題の落とし穴です(errfix の機械検証でも 14.17.0/14.17.6/14.18.0 はいずれもフラグ無しで undefined だった)。node:14 の固定イメージや 14 を保守用に残した CI では、パッチをいくら上げても既定では現れません。
再現(最小構成)
次の1ファイルを置きます。中断状態が signal.aborted に伝わるかを確かめる構成です。
// index.js
const controller = new AbortController();
controller.abort();
console.log(controller.signal.aborted);
これを Node 14 で node index.js すると ReferenceError: AbortController is not defined が出て、終了コードは 1 になります。Node 15 以降なら true と表示され、abort() が signal.aborted に伝わっていることがわかります。
解決
1. Node を 15 以降(実務ではサポート中の LTS)に上げる
AbortController を既定で使い続けるなら、実行する Node(CI・本番の Docker ベースイメージを含む)を上げます。フラグ無しの既定になったのは 15.0.0 ですが、15・16 はすでに EOL(サポート終了)なので、本番ではその時点でサポート中の LTS へ揃えます。前述のとおり 14 系はパッチを上げても既定では現れないので、メジャー版をまたぐ更新になります。他の依存パッケージが新しい版に対応しているかを併せて確認してください。
2. ポリフィルを入れる(古い Node を残す場合)
Node 14 をサポート対象に残すなら、AbortController 相当を提供するパッケージを使います。node-abort-controller が広く使われており、AbortController / AbortSignal を補います(起動フラグを足したくない場合の選択肢)。
// index.js(ポリフィルを使う)
const { AbortController } = require('node-abort-controller');
const controller = new AbortController();
controller.abort();
console.log(controller.signal.aborted);
グローバルの AbortController を前提に書かれた別のコードにも通したいときは、globalThis.AbortController へ代入して橋渡しできます。
const { AbortController } = require('node-abort-controller');
if (typeof globalThis.AbortController !== 'function') {
globalThis.AbortController = AbortController;
}
切り分け(似ているが原因が違うもの)
AbortControllerは使えるが、渡した先がsignalを無視する:AbortController自体が使えても、それを受け取る API 側が中断に対応していないと止まりません。signalを受け取り始めた版は API ごとに違い、たとえばfs.readFileは 15.2.0、http.requestは 15.3.0、fs.watchは 15.9.0 と別々です(いずれも 14.17.0 へバックポート済み)。「一括で何版から」とは言えないので、「AbortControllerは動くのに処理が止まらない」ときは、渡す相手の API がその版でsignalを見るかを個別に確認してください。なお 14.17.0 では、これらの API がsignalを受け取れる一方でグローバルAbortControllerはフラグ無しだと無い、という食い違いも起こります。- 同じ版境界で別のグローバル API が落ちる:
AbortControllerと同じく「新しい Node に組み込まれたグローバル」は、古い版でis not definedになります。fetch(Node 18+)、structuredClone(17+)、crypto.randomUUID(14.17/16+、メソッドなのでis not a function)などが該当します。原因は共通で、対処も Node を上げるかポリフィルを入れるかの二択です。fetchは ReferenceError: fetch is not defined の直し方、structuredCloneは ReferenceError: structuredClone is not defined の直し方、crypto.randomUUIDは TypeError: crypto.randomUUID is not a function の直し方 を参照してください。 - ブラウザ側で
AbortController is not definedが出た:ブラウザのAbortControllerは主要ブラウザで広く使えます。古いブラウザや一部の実行環境で無い場合は、Node と同じくポリフィルで補います。この記事は Node 実行時の話で、スタックトレースにファイルパスやnode:internal/...が出ているなら Node 側の問題です。