Ein Stack-Trace ist die Liste der Funktionsaufrufe, die gerade liefen, als etwas einen Fehler auslöste. Der Browser schreibt ihn von innen nach außen: die Funktion, die fehlgeschlagen ist, steht oben, die Funktion, die sie aufgerufen hat, darunter, und so weiter bis zu dem, was das Ganze überhaupt in Gang gesetzt hat.

Er sieht aus wie eine Textwand und wird auch so behandelt. Die meisten Leute lesen nur die erste Zeile, suchen nach der Meldung und schauen sich den Rest nie an. Das ist schade, denn die erste Zeile sagt, was kaputtgegangen ist, und die Zeilen darunter sagen, warum überhaupt jemand dort war.

Was die Zeilen eigentlich sind

Ein Browser-Trace sieht ungefähr so aus:

TypeError: Cannot read properties of undefined (reading 'total')
    at formatPrice (checkout.js:214:19)
    at renderSummary (checkout.js:188:7)
    at onCartLoaded (cart.js:97:5)
    at HTMLButtonElement.<anonymous> (cart.js:41:12)

Vier Dinge stecken darin, und jedes beantwortet eine andere Frage.

Die erste Zeile ist der Fehler: ein Typ und eine Meldung. Sie sagt, was im Moment des Fehlers schiefging, und ist die am wenigsten hilfreiche Zeile, wenn es um die Ursache geht. Im Beispiel hat etwas in formatPrice versucht, .total aus einem Wert zu lesen, der nicht vorhanden war, was der Fall des undefined-Werts ist und fast nie an formatPrice liegt.

Jede at-Zeile ist ein Frame: eine Funktion, die darauf wartete, dass die Funktion darüber zurückkehrt. formatPrice wurde von renderSummary aufgerufen, das wiederum von onCartLoaded aufgerufen wurde.

Die Zahlen sind Datei, Zeile und Spalte. checkout.js:214:19 bedeutet Zeile 214, Spalte 19, was wichtig wird, wenn mehrere Aufrufe in einer Zeile stehen.

Der unterste Frame ist der Auslöser, und er ist meist die aufschlussreichste einzelne Zeile im ganzen Trace. HTMLButtonElement.<anonymous> bedeutet, dass ein Click-Handler das Ganze gestartet hat: etwas, das eine Person getan hat, und nicht etwas, das die Seite beim Laden getan hat. Das ist bereits ein Reproduktionsschritt.

Von unten nach oben lesen

Gewöhnen Sie sich an, den Trace in der Reihenfolge zu lesen, in der der Code tatsächlich lief, also von unten nach oben.

Es wurde geklickt. onCartLoaded lief. Es rief renderSummary auf, das wiederum formatPrice aufrief, das nichts vorfand, wo es eine Warenkorbsumme erwartete. So gelesen, lautet die Frage nicht mehr „Was stimmt nicht mit formatPrice“, sondern „Was hatte onCartLoaded in der Hand, und woher kam es?“ Genau dort liegt fast immer die Lösung.

Schwach
Fehler in formatPrice, Zeile 214. Prüfung auf undefined hinzugefügt.
Besser
Der Click-Handler in cart.js:41 lief, bevor die Antwort für den Warenkorb zurückkam, sodass renderSummary ein leeres Objekt erhielt. Die Reihenfolge wurde korrigiert; formatPrice blieb unverändert.

Die erste Version lässt den Fehler verschwinden. Die zweite lässt den Bug verschwinden. Das ist nicht dieselbe Arbeit, und der Trace ist es, der die beiden unterscheidet.

Die Frames, die nicht Ihre eigenen sind

Die meisten echten Traces sind länger als das Beispiel und voller Frames aus Bibliotheken: React, jQuery, die Laufzeitumgebung eines Bundlers, ein Polyfill. Meistens sind sie nur Rauschen, und beide großen Browser können sie für Sie ausblenden.

In Chrome DevTools heißt die Funktion Ignore list, zu finden unter Settings. Sie blendet Frames aus Skripten aus, die Sie festlegen, und verhindert, dass der Debugger in sie hineinspringt. Standardmäßig ignoriert sie bereits /node_modules/ und /bower_components/, daher besteht der erste sinnvolle Schritt oft darin, eigene Vendor-Pfade hinzuzufügen. Der Debugger von Firefox verfolgt dieselbe Idee pro Datei, als Ignore source. So oder so ist jeder Trace danach nur noch halb so lang.

Was übrig bleibt, ist Ihr eigener Code, also der Teil, den Sie ändern können.

Eine Ausnahme sollten Sie im Auge behalten: Wenn ausschließlich Frames aus Bibliotheken übrig bleiben, kam der Aufruf wahrscheinlich aus dem Inneren dieser Bibliothek und nicht aus Ihrem Code, und die Ursache liegt meist in den Daten oder den Optionen, die Sie ihr übergeben haben.

Warum der Trace manchmal auf Unsinn zeigt

Drei Dinge machen einen Trace routinemäßig unlesbar, und alle drei liegen daran, wie der Code ausgeliefert wurde, nicht an dem Bug selbst.

Minifizierung. In der Produktion ist Ihr Code eine einzige riesige Zeile mit einbuchstabigen Namen, sodass im Trace nur noch at n (app.min.js:1:84213) steht. Die Zeilennummer ist echt und trotzdem nutzlos. Die Lösung sind Source Maps: Ein Build erzeugt eine .map-Datei, der Browser wendet sie an, und DevTools zeigt die ursprünglichen Dateinamen und Zeilen. Prüfen Sie, ob Ihre erzeugt werden und ob der Browser sie erreichen kann, bevor Sie einen Trace für hoffnungslos erklären.

Async-Grenzen. Ein Trace endet dort, wo der Call-Stack endet, und ein setTimeout, ein Promise-Callback oder ein Event-Listener startet einen neuen. Der Frame, der die Arbeit eingeplant hat, steht standardmäßig nicht im Trace. Die Async stack traces in Chrome sind in DevTools aktiviert und fügen beide Teile zusammen, weshalb ein Trace in DevTools vollständiger aussieht als derselbe Fehler in einer Log-Datei.

Cross-Origin-Skripte. Wird ein Skript von einer anderen Domain ausgeliefert, ohne die Erlaubnis, seine Fehler offenzulegen, verweigert der Browser jede Auskunft darüber: Die Meldung wird zur bloßen Zeichenkette Script error., ohne Datei, ohne Zeile und ohne Trace. Die Lösung liegt auf der Server-Seite: Access-Control-Allow-Origin auf dem Skript plus crossorigin="anonymous" auf dem Tag. Gut zu wissen, denn das Symptom wirkt wie ein mysteriöser Fehler, ist aber tatsächlich eine Berechtigungsregel.

Der Trace, den Sie nicht erfasst haben

All das oben setzt voraus, dass Sie den Trace haben. Der unangenehme Fall ist ausgerechnet der wichtigste: Der Fehler ist auf dem Rechner einer anderen Person passiert, in einem Browser, den Sie nicht haben, und bei Ihnen kam nur ein Satz an.

Ein Stack-Trace ist nichts, worum man einen Nutzer bitten kann. Er liegt in einem Panel, das dieser nie geöffnet hat, er ist verschwunden, sobald der Tab geschlossen wird, und die Konsole übersteht keine Seitennavigation, wenn nicht vorher jemand daran gedacht hat, Preserve log zu aktivieren. Die praktische Wahl besteht also zwischen einem Error-Tracker oder einem Report-Tool, das die Konsole im Moment des Fehlers erfasst, und dem Verzicht darauf.

Ohne das steht im Bericht nur „die Checkout-Seite ist weiß geblieben“, und die vier Zeilen, die es in einer Minute geklärt hätten, hat der Browser Sekunden später schon verworfen.

Session Replay

Kostenlose Chrome-Erweiterung. Ein Klick auf die Seite, die Probleme macht, erfasst den Screenshot, die Konsole und das Netzwerkprotokoll und gibt Ihnen einen Link zum Einfügen in das Ticket.

Erweiterung installieren

Einen Trace absichtlich erzeugen

Zwei Dinge sind gut zu wissen, wenn Sie einen Trace erzeugen wollen, statt auf einen zu warten.

console.trace() gibt den aktuellen Call-Stack aus, ohne einen Fehler auszulösen, und ist der schnellste Weg, um in einer fremden Codebasis die Frage „Wer ruft diese Funktion auf?“ zu beantworten.

new Error().stack liefert dasselbe als String, und genau das übermitteln Error-Tracker an ihren Server. So hängen Sie auch einen Trace an einen abgefangenen Fehler, der sonst verloren ginge: Wer eine Exception abfängt und nur e.message loggt, wirft den gesamten Stack weg, und diese Angewohnheit ist für viele unlesbare Produktionslogs verantwortlich.

Was Sie daraus mitnehmen sollten

Die oberste Zeile benennt das Symptom. Die unterste Zeile benennt, was es ausgelöst hat. Die Mitte ist der Weg von der einen zur anderen, und die Lösung liegt meist irgendwo auf diesem Weg und nicht an einem der beiden Enden.

Lesen Sie ihn von unten nach oben, blenden Sie die Frames aus, die nicht Ihre eigenen sind, und prüfen Sie Ihre Source Maps, bevor Sie dem Trace die Schuld geben. Und wenn der Trace, den Sie brauchen, zur Browser-Sitzung einer anderen Person gehört, müssen Sie schon vor dem Bug dafür sorgen, nicht danach: Wenn der Bericht ankommt, ist er längst weg.