Stack overflow / Maximum call stack size exceeded
「Maximum call stack size exceeded」で落ちる原因を終了条件・相互再帰・巨大データの3系統に切り分け、反復化や明示スタックで確実に直す手順を、スタックとヒープの違いから持ち帰れる。
- コールスタックは関数呼び出しごとにフレーム(戻り先・引数・ローカル変数)を積む固定上限の領域。再帰が深すぎて上限を超えると Stack overflow(JSでは Maximum call stack size exceeded)になる。
- 原因の大半は終了条件の欠落・誤りによる無限再帰。次いで相互再帰の見落とし、巨大データやリスト長に比例する深い再帰。まずスタックトレースが同じ関数の反復か否かを見る。
- 根治は再帰の反復化(ループ+明示スタック)。多くの言語は末尾呼び出し最適化に頼れないため、深さ制限や上限緩和は対症療法と割り切る。
一言でいうと(このエラーが何を意味するか)
関数を呼ぶたびに、実行環境はコールスタックへ1つのスタックフレームを積みます。フレームには戻り先アドレス、引数、ローカル変数、退避したレジスタなどが入ります。関数が return するとフレームは取り除かれます。このスタックはサイズ上限が決まった連続領域で、再帰などで呼び出しが深くなりフレームを積み続けると上限に達し、これ以上積めなくなった瞬間に Stack overflow が発生します。
JavaScript(V8 など)ではこの状態が RangeError: Maximum call stack size exceeded、Java では StackOverflowError、C/C++ では多くの場合セグメンテーションフォルトとして現れます。いずれも「呼び出しが深すぎてスタックを使い切った」という同一現象です。ヒープ枯渇(メモリ不足)とは領域が別で、原因も対処も異なります。
上限は呼び出し回数ではなくスタックのバイト容量です。フレーム1つが大きい(巨大なローカル配列や多数の引数)ほど、より浅い再帰で超えます。同じロジックでも引数を増やすと限界深度が下がるのはこのためです。
横にスクロール
よくある原因(複数を具体的に、頻度順)
1. 終了条件(ベースケース)の欠落・誤り=無限再帰。 最頻の原因です。条件はあるが一度も真にならない、更新式が誤って値が縮まない、といった論理ミスで自分を呼び続けます。
// 悪い例: n が減らず終了条件に到達しない
function sum(n) {
return n + sum(n); // sum(n-1) の書き忘れ
}
sum(10); // RangeError: Maximum call stack size exceeded
2. 相互再帰・間接再帰の見落とし。 A が B を、B が A を呼ぶ循環。単体では正しく見え、スタックトレースに複数の関数名が交互に並ぶため気づきにくいです。イベント駆動で「setter が描画を呼び、描画が値を書き戻して再び setter を発火」する循環もこの類型です。
3. データ量に比例する深い再帰。 ロジックは正しくても、要素数 n の連結リストや深くネストした JSON・ツリーを1要素1フレームで再帰処理すると、深さが n に比例し大きな n で上限を超えます。終了条件は正しいので「小さい入力では通り、本番データで落ちる」形になります。
| 症状 | 疑う原因 | 決め手 |
|---|---|---|
| 即座に落ちる/トレースが同一関数の反復 | 終了条件の欠落・誤り(無限再帰) | 引数がベースケースへ近づいているか |
| トレースに2つ以上の関数が交互に出る | 相互再帰・間接再帰の循環 | 呼び出し経路が円環になっていないか |
| 小入力は成功し大入力で落ちる | データ量比例の深い再帰 | 深さが入力サイズに線形か |
診断の手順(切り分け方・見るべきログ/コマンド)
スタックトレースの形を読む。 最優先はトレースの反復パターンです。同一の関数名が延々と続くなら単純再帰の無限ループ、2つ以上が交互なら相互再帰を疑います。トレースは深さで途中省略されることが多いので、末尾(最も内側)ではなく繰り返しの単位に注目します。
入力サイズを変えて再現条件を見る。 小さい入力で通り大きい入力で落ちるなら、無限再帰ではなく深さがデータ量に比例する再帰です。落ちる直前の深さを測るとフレーム数の見当がつきます。
// 現在のスタック深さを推定する(デバッグ用)
function depth() {
try { return 1 + depth(); } catch (e) { return 1; }
}
console.log(depth()); // 環境ごとの概算の限界深度
末尾呼び出し最適化(TCO)に依存していないか確認する。 「末尾再帰だから最適化されるはず」という前提は多くの環境で成り立ちません。ECMAScript 仕様上は末尾呼び出しの最適化が規定されていますが、V8(Chrome / Node.js)や SpiderMonkey は実装しておらず、末尾再帰でもフレームは積まれます。CPython も設計方針として TCO を行いません。頼れるのは処理系が明示的に保証する場合のみです。
Maximum call stack size exceeded(スタック)と JavaScript heap out of memory / OutOfMemoryError(ヒープ)は別物です。前者は呼び出しの深さ、後者はオブジェクトの総量が原因です。スタックはスレッドごとに小さめの固定領域、ヒープは動的確保される大きな領域で、深い再帰を配列に置き換えるとスタック負荷はヒープへ移ります。取り違えると上限を増やしても直りません。
解決と予防(対処と再発防止)
まずベースケースを検証する。 無限再帰なら、すべての再帰呼び出しが必ず終了条件へ近づくこと、条件が実際に真になり得ることを確認します。ここが根本原因なら上限緩和では直りません。
再帰を反復(ループ)へ書き換える。 最も確実な根治です。末尾再帰は単純なループへ機械的に置換できます。木やグラフの走査など本質的に再帰的な処理は、呼び出しスタックの代わりに明示的なスタック(配列)を使い、深さをヒープ側で管理します。これにより深さの上限がスタック容量からヒープ容量へ移り、桁違いに深い処理が可能になります。
// 再帰DFS → 明示スタックによる反復DFS
function traverse(root) {
const stack = [root];
while (stack.length > 0) {
const node = stack.pop();
visit(node);
for (const child of node.children) stack.push(child);
}
}
深さ制限とガードを入れる。 反復化が難しい箇所は、再帰の深さ引数に上限を設け、超過時に明示的な例外を投げて無限再帰を早期かつ意味のあるメッセージで止めます。想定最大深度を超える入力を弾く入力バリデーションも有効です。スタック上限自体の引数変更(Node.js の --stack-size、pthread のスタックサイズ、ulimit -s)は対症療法で、根本原因を残したまま限界を先送りするだけと理解して使います。
- コールスタックは固定上限、ヒープは動的領域。Stack overflow はスタック、OOM はヒープの枯渇
- 最頻原因は終了条件の欠落・誤りによる無限再帰。次点は相互再帰とデータ量比例の深い再帰
- V8・CPython は末尾呼び出し最適化を行わないため、末尾再帰でもフレームは積まれる
- 根治は反復化と明示スタック。上限緩和(--stack-size / ulimit -s)は対症療法
スタック/ヒープのメモリ配置とスレッドごとのスタック上限はOSを、再帰・反復・計算量の基礎はプログラミングを、Maximum call stack size exceeded を出す V8 などブラウザ実行環境の挙動はWebを参照してください。
エラー辞典の記事ガイド
Stack overflow / Maximum call stack size exceededを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
再帰
比較で見る軸
難易度: advanced / カテゴリ: エラー辞典 / タグ数: 6
導入後に効く点
原因の大半は終了条件の欠落・誤りによる無限再帰。次いで相互再帰の見落とし、巨大データやリスト長に比例する深い再帰。まずスタックトレースが同じ関数の反復か否かを見る。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- エラー辞典
- タグ数
- 6
判断チェックリスト
- 自社の用途が「再帰 / スタック」に近いか確認する。
- 強みである「コールスタックは関数呼び出しごとにフレーム(戻り先・引数・ローカル変数)を積む固定上限の領域。再帰が深すぎて上限を超えると Stack overflow(JSでは Maximum call stack size exceeded)になる。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。