Back

JavaScriptで「Maximum call stack size exceeded」を修正する

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でスタックサイズを増やすことは診断ツールであり、解決策ではありません。クラッシュを遅らせるだけで、バグは修正されません。

実際に機能する実用的な修正方法

再帰を反復処理に変換する

ほとんどの再帰アルゴリズムは、明示的なスタックを使用して反復的にすることができます:

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.

OpenReplay