12k
All articles

So lesen Sie einen JavaScript-Stack-Trace

JavaScript-Stack-Traces lesen: den ersten nützlichen Frame erkennen, async-Lücken, Minified-Output, Source Maps und Error.cause verstehen.

OpenReplay Team
OpenReplay Team
So lesen Sie einen JavaScript-Stack-Trace

Ein JavaScript-Stack-Trace wird zeitlich rückwärts gelesen: Der oberste Frame ist der Aufruf, der die Ausnahme ausgelöst hat, und jeder Frame darunter ist der Aufruf, der dorthin geführt hat.

Üblicherweise überfliegt man die erste Zeile, kopiert sie in ein Suchfeld und hofft auf das Beste. Das funktioniert so lange, bis der oberste Frame zu React gehört, zu JSON.parse oder zu einem Bundle, in dem jede Funktion o heißt.

Dieser Artikel arbeitet einen einzigen Trace von Anfang bis Ende durch und ergänzt in jedem Abschnitt eine weitere Lesart: welchen Frame man tatsächlich öffnen sollte, was async-Frames aussagen, wie ein minifizierter Trace aussieht und warum der Stack hinter Error.cause niemals in dem Stack auftaucht, den Sie ausgegeben haben.

Die wichtigsten Erkenntnisse

  • Der oberste Frame zeigt, wo der Fehler ausgelöst wurde, nicht wo der Bug entstanden ist; der erste Frame, der auf eine von Ihnen geschriebene Datei verweist, ist der Ausgangspunkt der Untersuchung.
  • Mit async gekennzeichnete Frames werden von V8 aus den Stellen rekonstruiert, an denen jedes await pausiert und fortgesetzt wurde. Deshalb werden await, Promise.all() und Promise.any() zusammengefügt, während eine bloße .then()-Kette eine Lücke hinterlässt.
  • V8 behält standardmäßig nur 10 Frames – eine Zahl, die Sie über das nicht standardisierte Error.stackTraceLimit ändern können.
  • new Error(message, { cause }) bewahrt den ursprünglichen Fehler, aber die Engine führt die beiden Stacks nicht zusammen: err.stack zeigt nur den Wrapper.

Wie sieht ein JavaScript-Stack-Trace aus?

Ein JavaScript-Stack-Trace besteht aus Fehlername und Meldung in der ersten Zeile, gefolgt von einem Frame pro Aufruf, der neueste zuerst. Hier ist der Trace, auf den dieser Artikel immer wieder zurückkommt: Eine Warenkorbdatei wird von der Festplatte gelesen, geparst und an den Rest der Anwendung übergeben.

SyntaxError: Unexpected token 'b', "{ bad json" is not valid JSON
    at JSON.parse (<anonymous>)
    at parseCart (/app/src/cart.js:5:15)
    at loadCart (/app/src/cart.js:10:20)
    at main (/app/src/main.js:6:16)
    at Object.<anonymous> (/app/src/main.js:12:1)

Liest man die Frames in dieser Reihenfolge, ist die Kette offensichtlich: JSON.parse hat die Ausnahme ausgelöst, parseCart hat es aufgerufen, loadCart hat parseCart aufgerufen und so weiter nach unten. Der unterste Frame markiert den Anfang genau dieser Aufrufkette, was nicht immer der Einstiegspunkt des Programms ist: Code, der über einen Click-Handler, einen Timer-Callback oder ein aufgelöstes Promise erreicht wird, erhält eine frische Kette, die beim Callback beginnt.

Ein einzelner Frame liefert Ihnen vier Informationen: den Funktionsnamen, die Datei, die Zeile und die Spalte. In minifiziertem Code ist ohne Source Map allein die Spalte etwas wert. Ein Vorbehalt, den man früh kennen sollte: Error.prototype.stack ist nicht standardisiert. Jede Engine liefert es aus, jede gibt einen leicht abweichenden String aus, und die Arbeit bei TC39 zur Festlegung des Formats ist noch nicht abgeschlossen. Die Beispiele hier folgen der V8-Form, was Chrome, Edge und Node abdeckt.

Der oberste Frame ist meist nicht Ihr Code

Der oberste Frame eines Stack-Trace ist in der Regel nicht Ihr Code. Es ist die Bibliothek, das Framework oder die eingebaute Funktion, die den ungültigen Wert bemerkt hat – er sagt Ihnen also, was kaputtgegangen ist, nicht warum. Im obigen Trace ist at JSON.parse (<anonymous>) der eingebaute Parser, der meldet, dass ein ihm übergebener String kein gültiges JSON ist. Dort gibt es nichts zu reparieren.

Der Frame, den Sie suchen, ist der erste, der auf eine von Ihnen geschriebene Datei verweist. Das ist parseCart in /app/src/cart.js:5:15. Öffnen Sie diese Zeile, finden Sie dort den Aufruf von JSON.parse, was bedeutet, dass der fehlerhafte String als Argument hereinkam. Der Wert stammt also aus dem darunterliegenden Frame: loadCart in Zeile 10, wo die Datei gelesen wurde. Das ist der eigentliche Ausgangspunkt, und die Frage, die er beantwortet, lautet: Woher kam der Dateiinhalt und warum hat ihn niemand validiert?

Diese Lesereihenfolge lässt sich verallgemeinern. Überspringen Sie beim Durchscrollen node_modules-Pfade sowie <anonymous>- und native-Frames, bis Sie auf Ihre eigene Datei stoßen, und arbeiten Sie sich dann durch die Frames nach unten, die den Wert geliefert haben.

Ein Trace zeichnet den Weg auf, den das Programm in den Fehler genommen hat, nie den Weg des Nutzers. Deshalb können zwei Meldungen mit identischen Frames einmal ein Fünf-Minuten-Fix und einmal ein nicht reproduzierbares Phantom sein. Session Replay schließt diese Hälfte der Lücke: Sie lesen die Frames für das Wo und sehen sich die Session an, um zu verstehen, wie die Anwendung in einen Zustand geraten ist, in dem das überhaupt schiefgehen konnte.

Async-Frames und die Await-Grenze

Markieren Sie loadCart als async, und der Trace überspannt das await, wobei die rekonstruierten Frames mit async vorangestellt werden:

SyntaxError: Unexpected token 'b', "{ bad json" is not valid JSON
    at JSON.parse (<anonymous>)
    at parseCart (/app/src/cart.js:5:15)
    at async loadCart (/app/src/cart.js:10:20)
    at async main (/app/src/main.js:6:16)

Diese mit async markierten Frames werden nicht auf dieselbe Weise erfasst wie synchrone Frames. V8 rekonstruiert sie aus den await-Stellen und kann das praktisch kostenlos tun, weil ein await genau an der Stelle wieder aufgenommen wird, an der es pausiert hat.

Die Rekonstruktion hat Grenzen, und genau dort wird der Trace dünn. Das Zusammenfügen erfasst await-Punkte, Promise.all() und Promise.any() – und sonst nichts. Ein Promise, das zurückgegeben, aber nicht awaited wird, oder eine .then()-Kette hinterlässt daher genau dort ein Loch, wo vorher der aufrufende Kontext war. Wenn die Frames an einer async-Grenze abrupt enden, suchen Sie nach einem fehlenden await in der darüberliegenden Schicht. Ein Flag zum Aktivieren gibt es nicht: --async-stack-traces ist seit V8 v7.3 standardmäßig aktiviert, sodass async-Frames in Chrome, in allen gepflegten Node-Releases und in anderen aktuellen V8-Laufzeitumgebungen erscheinen.

Der andere Grund für fehlende Frames ist das Limit. V8 behält 10 Frames und verwirft den Rest, und Error.stackTraceLimit ist der Regler für diese Zahl: Ein neuer Wert gilt für Fehler, die nach dem Setzen erzeugt werden, und alles, was keine Zahl oder kleiner als null ist, lässt Sie ganz ohne Frames zurück.

if (process.env.NODE_ENV !== 'production') {
  Error.stackTraceLimit = Infinity;
}

Beschränken Sie das auf die Entwicklung. Tiefe Traces kosten Speicher, begraben die interessanten Frames im Rauschen und schieben Dateipfade und Funktionsnamen in Logs, die möglicherweise anderswo landen. Die Eigenschaft ist nicht standardisiert: Sie stammt aus V8, und JavaScriptCore hat sie aus Kompatibilitätsgründen übernommen. Das Setzen bricht also nirgends etwas, aber der Standardwert und die Feinheiten hängen von der Engine ab.

Wie liest man einen minifizierten Trace?

Gegen ein Produktions-Bundle erzeugt derselbe Fehler Frames wie diese. Betrachten Sie die Form als beispielhaft für Bundle-Ausgaben, nicht als exaktes Format eines bestimmten Werkzeugs:

SyntaxError: Unexpected token 'b', "{ bad json" is not valid JSON
    at JSON.parse (<anonymous>)
    at o (/assets/index-4f1c8a2b.js:1:20874)
    at async s (/assets/index-4f1c8a2b.js:1:21036)

Die Signatur ist unverkennbar: einbuchstabige Funktionsnamen, ein einziger Dateiname, Zeile 1 und eine fünf- oder sechsstellige Spaltennummer. Zeile 1 und eine riesige Spaltennummer bedeuten, dass der gesamte Modulgraph in einer einzigen Zeile steht – die Spalte ist also die einzige Koordinate, die noch Information trägt. parseCart und loadCart existieren in diesem Spalten-Offset weiterhin, aber nichts in diesem String verrät Ihnen ihre Namen.

Um sie wiederherzustellen, brauchen Sie eine Source Map, die das Tooling erreichen kann – zur Build-Zeit erzeugt und irgendwo hochgeladen, wo der Trace dagegen aufgelöst werden kann. Unser Leitfaden dazu, wie Source Maps funktionieren, behandelt das Format und die Build-Konfiguration.

Error.cause führt Stacks nicht zusammen

Das Umhüllen eines Fehlers mit new Error(message, { cause: originalError }) bewahrt das ursprüngliche Fehlerobjekt samt Typ und eigenem Stack, aber die Engine führt die beiden Stacks nicht zusammen. err.stack zeigt nur den Wrapper, und das Original ist ausschließlich über err.cause.stack erreichbar.

export async function loadCart(path) {
  const raw = await readFile(path, 'utf8');
  try {
    return parseCart(raw);
  } catch (err) {
    throw new Error(`Cart file ${path} is not valid JSON`, { cause: err });
  }
}

Aufrufer erhalten nun eine Meldung, die die Datei benennt, und err.cause instanceof SyntaxError trifft weiterhin zu. Was sie nicht bekommen, ist der JSON.parse-Frame: Der Stack des Wrappers beginnt beim throw innerhalb von loadCart. Error.cause kam mit ES2022 und funktioniert in aktuellen Browsern sowie in jedem gepflegten Node-Release zurück bis Node 16.9.0. Es wird als eigene, nicht aufzählbare Eigenschaft angelegt und bleibt damit außerhalb von Object.keys(), for...in und einem naiven JSON.stringify() des Fehlers.

Wie viel einer Kette eine Konsole ausgibt, hängt von der Laufzeitumgebung und der jeweiligen Konsole ab. Der portable Weg ist daher, sie selbst zu durchlaufen:

function printChain(error) {
  let current = error;
  while (current instanceof Error) {
    console.error(current.stack);
    current = current.cause;
  }
}

Das gibt zuerst die Frames des Wrappers aus, dann die des Parsers – in der Reihenfolge, in der sie ausgelöst wurden.

Zwei Gewohnheiten, die die Spur zerstören

Einen Fehler abzufangen und einen neuen zu werfen, ohne eine cause zu übergeben, löscht den ursprünglichen Trace aus dem Programm. Die Frames, die Ihnen gesagt hätten, wo der fehlerhafte Wert hereinkam, existieren nirgendwo mehr, und keine noch so gründliche Log-Suche bringt sie zurück:

catch (err) {
  throw new Error('Could not load cart');   // JSON.parse frame is gone
}

Den Fehler in einer Log-Zeile zu verschlucken, richtet denselben Schaden an, nur leiser:

catch (err) {
  console.log('cart load failed');          // message, no stack, no type
  return [];
}

Beide sind nur ein Schlüsselwort davon entfernt, in Ordnung zu sein. Übergeben Sie { cause: err }, wenn Sie erneut werfen, und loggen Sie err selbst statt eines Satzes darüber.

Wenn das nächste Mal ein Trace vor Ihnen landet, beginnen Sie nicht bei Zeile eins. Scrollen Sie nach unten zum ersten Frame mit Ihrem eigenen Dateinamen, öffnen Sie diese Zeile und fragen Sie, welcher Wert ihr übergeben wurde und von wem. Wenn die Frames an einer async-Grenze oder bei einer einbuchstabigen Funktion enden, sehen Sie eine Stitching-Lücke oder eine fehlende Source Map – nicht die ganze Geschichte.

FAQs

Warum meldet mein Error-Handler nur 'Script error.' ohne Stack?

Browser verbergen die Details von Ausnahmen, die von Cross-Origin-Skripten ausgelöst werden. Deshalb erhält window.onerror den generischen Text 'Script error.' ohne brauchbare URL, Zeilennummer oder Stack. Um die echte Meldung und die Frames zu bekommen, laden Sie das Skript mit dem crossorigin-Attribut auf anonymous und stellen Sie sicher, dass der Server, der es ausliefert, einen Access-Control-Allow-Origin-Header sendet, der Ihre Origin abdeckt. Die meisten öffentlichen CDNs senden diesen Header bereits.

Funktioniert Error.captureStackTrace außerhalb von Chrome und Node?

Es ist nicht mehr V8-exklusiv. Error.captureStackTrace entstand in V8 als Teil der nicht standardisierten Stack-Trace-API, und die anderen Engines sind inzwischen nachgezogen: JavaScriptCore hat es in Safari 17.2 ausgeliefert, veröffentlicht am 11. Dezember 2023, und SpiderMonkey in Firefox 138, veröffentlicht am 29. April 2025. Der Aufruf schreibt einen Stack-String auf das Objekt, das Sie übergeben. Da es weiterhin nicht standardisiert ist, sollten Sie den Aufruf in gemeinsam genutztem Bibliothekscode mit einer typeof-Error.captureStackTrace-Prüfung absichern.

Warum sehen Stack-Traces in Firefox anders aus als in Chrome?

Error.prototype.stack liegt außerhalb jeder Spezifikation, sodass jede Engine es nach eigenem Gutdünken ausgibt und die Inhalte variieren. V8 schreibt jeden Frame in eine Zeile, die mit 'at' beginnt, während Firefox eine Form wie functionName@file:line:column ohne dieses Präfix verwendet. Behandeln Sie den Stack-String als für Menschen lesbare Ausgabe, nicht als parsebare API, und bauen Sie Fehlergruppierung niemals allein auf einer selbstgebauten Regex auf.

Kann ich einen Stack-Trace erfassen, ohne einen Fehler zu werfen?

Ja. In den meisten aktuellen Engines wird der Stack beim Konstruieren des Error-Objekts gefüllt, nicht beim Werfen. const { stack } = new Error() liefert Ihnen den Call Stack also direkt an Ort und Stelle, ohne throw und ohne catch. Der oberste Frame ist die Zeile, die den Fehler erzeugt hat, und das übliche Frame-Limit gilt weiterhin: V8 behält nur 10 Frames, sofern Error.stackTraceLimit nicht erhöht wird.

Open-source session replay

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.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.