🌐 Detecting your location…
📢 Advertisement — Configure AdSense in Appearance → Customize → AdSense Settings

Umgang mit Überschreitung der maximalen Call-Stack-Größe in JavaScript

⏱️5 min read  ·  1,094 words

Der FehlerRangeError: Maximale Aufrufstapelgröße überschritten (in Nicht-Browser-Umgebungen auch als „Stapelüberlauf“ bezeichnet) bezeichnet eine Funktion, die sich selbst (direkt oder indirekt) so oft aufruft, dass JavaScript nicht mehr über den Aufrufstapelplatz verfügt. Hier finden Sie alle Ursachen und Lösungen.

Warum das passiert

Jeder Funktionsaufruf fügt dem Aufrufstapel einen Frame hinzu. JavaScript-Engines haben eine begrenzte Stapelgröße (~10.000–15.000 Frames, abhängig von der Browser-/Knotenversion). Wenn rekursiven Funktionen kein richtiger Basisfall vorliegt oder wenn sich zwei Funktionen auf unbestimmte Zeit gegenseitig aufrufen, füllt sich der Stapel und löst RangeError aus.

// Simplest example: missing base case
function factorial(n) {
  return n * factorial(n - 1);  // never stops — factorial(0) calls factorial(-1) etc.
}
factorial(5);  // RangeError: Maximum call stack size exceeded

// Fix: add base case
function factorial(n) {
  if (n <= 1) return 1;       // base case stops recursion
  return n * factorial(n - 1);
}

Ursache 1: Unendlicher rekursiver Ereignishandler

// 🐛 Bug: React onChange updates state, triggers re-render, fires onChange again
function SearchInput() {
  const [value, setValue] = useState('');

  return <input
    value={value}
    onChange={(e) => {
      setValue(e.target.value);
      // If something here triggers another state update that causes re-render...
    }}
  />;
}

// 🐛 Bug: useEffect calling function that updates state without dependency guards
useEffect(() => {
  setData(processData(data));  // updates data → triggers effect → updates data...
}, [data]);

// ✅ Fix: use functional update or conditional guard
useEffect(() => {
  if (data.needsProcessing) {
    setData(prev => processData(prev));  // functional update avoids dependency
  }
}, [data.id]);  // depend on stable identifier, not the object

Ursache 2: Zirkuläre Objektverweise in JSON.stringify

// 🐛 Bug: circular reference crashes JSON.stringify
const obj = { name: "test" };
obj.self = obj;   // circular reference
JSON.stringify(obj);  // TypeError: Converting circular structure to JSON

// ✅ Fix 1: Replace circular references
function stringifySafe(obj) {
  const seen = new WeakSet();
  return JSON.stringify(obj, (key, value) => {
    if (typeof value === 'object' && value !== null) {
      if (seen.has(value)) return '[Circular]';
      seen.add(value);
    }
    return value;
  });
}

// ✅ Fix 2: Use structuredClone for deep copying (handles circular refs)
const safeCopy = structuredClone(obj);  // built-in, available Node 17+ / modern browsers

Ursache 3: Deep Object Cloning mit Rekursion

// 🐛 Bug: naive deep clone fails on deeply nested objects
function deepClone(obj) {
  if (typeof obj !== 'object') return obj;
  return Object.fromEntries(
    Object.entries(obj).map(([k, v]) => [k, deepClone(v)])  // crashes at ~10k depth
  );
}

// ✅ Fix 1: Use structuredClone (built-in, iterative internally)
const clone = structuredClone(deeplyNestedObj);

// ✅ Fix 2: Convert recursion to iteration for very deep objects
function deepCloneIterative(root) {
  const stack  = [{ source: root, target: Array.isArray(root) ? [] : {} }];
  const result = stack[0].target;
  const map    = new WeakMap([[root, result]]);

  while (stack.length) {
    const { source, target } = stack.pop();
    for (const [key, value] of Object.entries(source)) {
      if (typeof value === 'object' && value !== null) {
        if (map.has(value)) {
          target[key] = map.get(value);
          continue;
        }
        const newTarget = Array.isArray(value) ? [] : {};
        map.set(value, newTarget);
        target[key] = newTarget;
        stack.push({ source: value, target: newTarget });
      } else {
        target[key] = value;
      }
    }
  }
  return result;
}

Ursache 4: Versehentliches Aufrufen einer Funktion, anstatt darauf zu verweisen

// 🐛 Bug: function immediately calls itself
class Timer {
  start() {
    this.intervalId = setInterval(this.tick(), 1000);  // tick() CALLED, not referenced
    // this.tick() returns undefined (or calls tick immediately, which schedules tick...)
  }

  tick() {
    console.log('tick');
    this.start();  // calls start() which calls tick() again immediately
  }
}

// ✅ Fix: pass function reference, not call result
class Timer {
  start() {
    this.intervalId = setInterval(() => this.tick(), 1000);  // reference, not call
  }

  tick() {
    console.log('tick');
  }
}

Fix: Rekursion in Iteration konvertieren (Trampolinspringen)

// For recursive algorithms that process large inputs:

// 🐛 Recursive sum — crashes for large n
function sum(n) {
  if (n === 0) return 0;
  return n + sum(n - 1);  // crashes for n > ~10,000
}

// ✅ Iterative solution
function sumIterative(n) {
  let total = 0;
  for (let i = n; i > 0; i--) {
    total += i;
  }
  return total;
}

// ✅ Trampolining (for complex recursive algorithms that are hard to iterate directly)
function trampoline(fn) {
  return function trampolined(...args) {
    let result = fn(...args);
    while (typeof result === 'function') {
      result = result();
    }
    return result;
  };
}

const sumTrampoline = trampoline(function sum(n, acc = 0) {
  if (n === 0) return acc;
  return () => sum(n - 1, acc + n);  // returns a thunk instead of recursing
});

sumTrampoline(100000);  // works fine

Debuggen von Stapelüberläufen

// The error stack trace shows the recursive call
// RangeError: Maximum call stack size exceeded
//     at factorial (/app/math.js:2:10)
//     at factorial (/app/math.js:2:10)     ← same line repeated = infinite recursion
//     at factorial (/app/math.js:2:10)
//     ...

// Step 1: Look for the repeated line in the stack trace
// Step 2: Add console.log with a counter to see how deep it goes
let callCount = 0;
function myFunction(data) {
  callCount++;
  if (callCount > 1000) {
    console.trace('Deep recursion detected at:', callCount);
    throw new Error('Stack overflow prevention');
  }
  // ...
}

// Step 3: Check for circular dependencies
function detectCircular(obj, path = '', visited = new WeakSet()) {
  if (typeof obj !== 'object' || obj === null) return;
  if (visited.has(obj)) { console.log('Circular at:', path); return; }
  visited.add(obj);
  for (const [key, val] of Object.entries(obj)) {
    detectCircular(val, `${path}.${key}`, visited);
  }
}

Häufig gestellte Fragen

F: Wie hoch ist die maximale Call-Stack-Tiefe in JavaScript?
A: ~10.000–15.000 Frames in Chrome/Node.js; ~5.000–10.000 in Firefox; variiert je nach Motorversion und Rahmengröße. Die genauen Zahlen ändern sich zwischen den Versionen.

F: Kann ich die Aufrufstapelgröße erhöhen?
A: In Node.js:node --stack-size=65536 app.js (Größe in KB). In Browsern: nicht möglich. Bessere Lösung: Korrigieren Sie die Rekursion, anstatt das Limit zu erhöhen.

F: Was ist der Unterschied zwischen einem Stapelüberlauf und einem Heap-Speicher?
A: Stapelüberlauf (RangeError) = zu viele Funktionsaufrufrahmen. Heap nicht genügend Speicher (FATAL ERROR) = zu viele Objekte zugewiesen. Der Stapelüberlauf ist auf eine tiefe Rekursion zurückzuführen. Heap-Fehler entstehen durch die Zuweisung zu vieler Daten.

F: Wie kann ich ein Objekt im Jahr 2026 sicher tief klonen?
A: Verwenden SiestructuredClone(obj) – integriert in alle modernen Browser und Node.js 17+. Es verarbeitet Zirkelverweise, Datumsobjekte, Karten, Mengen und ArrayBuffer korrekt. Verwenden Sie JSON.parse(JSON.stringify(obj)) nur für einfache JSON-serialisierbare Objekte ohne zirkuläre Referenzen.

F: Ist die Tail-Call-Optimierung in JavaScript verfügbar?
A: Theoretisch ja (die ES6-Spezifikation beinhaltet die Gesamtbetriebskosten), aber nur Safari implementiert sie im Jahr 2026 vollständig. V8 (Chrome/Node.js) hat die Gesamtbetriebskosten entfernt. Verlassen Sie sich bei der Stapelsicherheit nicht auf die Gesamtbetriebskosten, sondern nutzen Sie stattdessen iterative Lösungen oder Trampolinspringen.

Fazit

Fehler, die die maximale Größe des Aufrufstapels überschritten haben, sind auf eine unendliche oder extrem tiefe Rekursion zurückzuführen. Die Lösung hängt von der Ursache ab:Fügen Sie einen Basisfall hinzu, um die Rekursion zu stoppen, konvertieren Sie die Rekursion für große Eingaben in eine Iteration, verwenden Sie StructuredClone für tiefe Kopien und achten Sie auf zirkuläre Objektreferenzen. Der Stack-Trace zeigt immer die wiederholte Zeile, die die unendliche Rekursion verursacht – lesen Sie ihn zuerst, dann ist die Lösung normalerweise offensichtlich.

✍️ Leave a Comment

Your email address will not be published. Required fields are marked *

🌐 Read in:🇩🇪 Deutsch🇧🇷 Português🇸🇦 العربية🇮🇳 हिन्दी🇧🇩 বাংলা