JavaScriptで「Maximum call stack size exceeded」を修正する
コンソールを見ていると、そこに表示されています:RangeError: Maximum call stack size exceeded。Firefoxではtoo much recursionと表示されるかもしれません。いずれにせよ、アプリケーションがクラッシュしてしまい、その理由と適切な修正方法を理解する必要があります。
このエラーはスタックオーバーフローを示しています:コードが非常に多くのネストされた関数呼び出しを行ったため、JavaScriptエンジンがそれらを追跡するためのスペースを使い果たしてしまったのです。モダンなJavaScriptでこれをトリガーする原因、効果的なデバッグ方法、そしてReact、Next.js、Node.jsアプリケーションでの予防方法を探っていきましょう。
重要なポイント
- コールスタックには環境によって異なる有限のサイズがあるため、特定の制限に到達することを前提としたコードは決して書かないでください。
- 一般的な原因には、無限再帰、相互再帰、Reactの無限再レンダリング、JSON直列化における循環参照などがあります。
- DevToolsで「例外時に一時停止」を有効にし、コールスタック内の繰り返し関数パターンを探すことでデバッグできます。
- 再帰を反復処理に変換する、トランポリンを使用する、またはreplacer関数で循環参照を処理することで構造的に修正できます。
コールスタックが実際に行うこと
関数が実行されるたびに、JavaScriptエンジンは関数のローカル変数、引数、戻りアドレスを含むスタックフレームを作成します。これらのフレームは互いに積み重なります。関数が戻ると、そのフレームは削除されます。
問題は何でしょうか?このスタックには有限のサイズがあるということです。正確な制限は環境によって異なります。Chromeでは約10,000〜15,000フレームを許可する場合があり、Firefoxでは約50,000を許可します。Node.jsは通常、デフォルトで約11,000フレームに制限されています。
**重要:**これらの数値は実装に依存しており、バージョン間で変更される可能性があります。特定の制限に到達することを前提としたコードは書かないでください。
スタックオーバーフローをトリガーする一般的なパターン
典型的な無限再帰
教科書的なケース:適切な終了条件なしに自分自身を呼び出す関数です。
function processItem(item) {
// ベースケースが欠落
return processItem(item.child)
}
相互再帰
2つの関数がループ内で互いに呼び出し合う場合:
function isEven(n) {
return n === 0 ? true : isOdd(n - 1)
}
function isOdd(n) {
return n === 0 ? false : isEven(n - 1)
}
isEven(100000) // スタックオーバーフロー
Reactの無限再レンダリング
多くのフロントエンド開発者がこのエラーに遭遇する場所です。レンダリング中の状態更新が無限ループを作成します:
function BrokenComponent() {
const [count, setCount] = useState(0)
setCount(count + 1) // 即座に再レンダリングをトリガー
return <div>{count}</div>
}
不適切なuseEffectの依存関係も同様の問題を引き起こします:
useEffect(() => {
setData(transformData(data)) // dataが変更され、effectが再度実行される
}, [data])
JSON循環参照によるスタックオーバーフロー
オブジェクトが自分自身を参照する場合、JSON.stringifyは無限に再帰します:
const obj = { name: 'test' }
obj.self = obj
JSON.stringify(obj) // Maximum call stack size exceeded
Node.jsとブラウザでのコールスタックオーバーフローのデバッグ
ステップ1:「例外時に一時停止」を有効にする
Chrome DevToolsで、Sourcesパネルを開き、「Pause on caught exceptions」を有効にします。Node.jsの場合は、--inspectフラグを使用してChrome DevToolsに接続します。
ステップ2:繰り返しフレームのコールスタックを検査する
デバッガーが一時停止したら、コールスタックパネルを調べます。繰り返しパターンを探します。同じ関数が数十回または数百回出現する場合、それが再帰ポイントを示しています。
ステップ3:非同期スタックトレースを使用する
モダンなDevToolsはデフォルトで非同期スタックトレースを表示します。これは、再帰がPromiseチェーンやsetTimeoutコールバックにまたがる場合に役立ちます。
console.trace() // 現在のスタックトレースを出力
注意:node --stack-sizeでスタックサイズを増やすことは診断ツールであり、解決策ではありません。クラッシュを遅らせるだけで、バグは修正されません。
Discover how at OpenReplay.com.
実際に機能する実用的な修正方法
再帰を反復処理に変換する
ほとんどの再帰アルゴリズムは、明示的なスタックを使用して反復的にすることができます:
function processTree(root) {
const stack = [root]
while (stack.length > 0) {
const node = stack.pop()
process(node)
if (node.children) {
stack.push(...node.children)
}
}
}
深い再帰にはトランポリンを使用する
トランポリンは再帰をステップに分割し、スタックの成長を防ぎます:
function trampoline(fn) {
return function(...args) {
let result = fn(...args)
while (typeof result === 'function') {
result = result()
}
return result
}
}
循環参照を安全に処理する
JSON直列化には、replacer関数またはflattedのようなライブラリを使用します:
const seen = new WeakSet()
JSON.stringify(obj, (key, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return '[Circular]'
}
seen.add(value)
}
return value
})
Reactでの無限再帰を防ぐ
useEffectの依存関係が安定していることを常に確認し、状態更新を条件付きにします:
useEffect(() => {
if (!isProcessed) {
setData(transformData(rawData))
setIsProcessed(true)
}
}, [rawData, isProcessed])
末尾呼び出し最適化では救われない理由
ES6は適切な末尾呼び出しを指定しており、理論的には末尾位置での無限再帰を可能にします。実際には、Safariのみがこれを実装しています。ChromeとFirefoxは実装しておらず、Node.jsはこれを無効にしました。TCOに頼らず、代わりにコードをリファクタリングしてください。
まとめ
「Maximum call stack size exceeded」エラーは、構造的な修正を必要とする重大度の高いバグです。catchで回避することはできませんし、本番環境でスタック制限を増やそうとすべきではありません。
スタックトレース内の繰り返しパターンを見つけ、適切な終了条件を追加するか、反復処理に変換するか、作業を非同期チャンクに分割してください。循環参照はデータ構造の問題として扱い、直列化の問題として扱わないでください。
このエラーが表示されたら、コードが何か根本的な変更が必要であることを伝えています。
よくある質問
技術的には可能ですが、信頼できる解決策ではありません。このエラーがスローされる時点で、アプリケーションは不安定な状態にあります。スタックが使い果たされており、エラーをキャッチしてもそれを復元することはできません。例外を処理しようとするのではなく、根本的な再帰の問題を修正してください。
ブラウザのDevToolsを開き、Sourcesパネルに移動し、「Pause on Exceptions」を有効にします。エラーが発生したら、コールスタックパネルで繰り返される関数名を調べます。繰り返し表示される関数が原因です。console.trace()を使用して特定のポイントでスタックを出力することもできます。
レンダリング本体で直接setStateを呼び出すと、即座に再レンダリングがトリガーされ、それが再びsetStateを呼び出し、無限ループが作成されます。状態更新を適切な依存関係を持つuseEffectフック内に移動するか、イベントハンドラー内に移動してください。レンダリング中に無条件に状態を更新しないでください。
Node.jsでは、--stack-sizeフラグを使用して制限を増やすことができますが、これはクラッシュを遅らせるだけです。ブラウザではスタックサイズの変更は許可されていません。どちらのアプローチも根本原因を修正しません。深い再帰に依存するのではなく、反復処理または非同期パターンを使用するようにコードをリファクタリングしてください。
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Check our GitHub repo and join the thousands of developers in our community.