
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.
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.