Fehlerbehandlung in Webapplikationen (Teil 1)

S*** happens

Die Webapplikation ist ausgerollt und im Einsatz und plötzlich meldet der Kunde Fehler, die niemand nachvollziehen kann. Was tun? Praktisch wäre, eine Applikation komplett ohne Fehler zu entwickeln - doch die vollständige Fehlerfreiheit einer Software wird leider vermutlich immer ein Mythos bleiben. Die Konsequenz hierdurch ist, dass man mit Fehlern rechnen und diese behandeln muss. Doch im Eigentlichen muss sich der Benutzer hiermit auseinandersetzen und zurechtkommen. Dies ist der Grund, warum es immer wichtiger wird, in der Entwicklung Fehler zu berücksichtigen und bestmöglich abzufangen, um einen reibungslosen Programmablauf sicherzustellen und dies für die Fehlerbehebung zu protokollieren. 
Für die klassischen Programmiersprachen wie C++ und Java stehen schon lange viele verschiedene Tools zur Verfügung, welche die Entwickler dabei unterstützen, Fehler zu vermeiden und zu analysieren.

Der Einsatz von Javascript in Anwendungen im Unternehmensumfeld ist noch relativ "jung" und noch nicht so etabliert wie die bereits genannten Programmiersprachen. Hier ist die Auswahl an verfügbaren Werkzeugen zur Fehleranalyse begrenzt. Da im Enterprise-Bereich immer mehr Weblösungen in Javascript Verbreitung finden, besteht die Notwendigkeit, passende Werkzeuge inklusive eines umfassenden Monitorings und Loggings zu finden. In diesem Beitrag möchten wir darauf eingehen, welche Möglichkeiten Entwickler in Javascript haben. Dabei wollen wir aufzeigen, wie Fehler zur Laufzeit in Produktivsystemen analysiert und erfasst werden können. Hierdurch erhält man nicht nur den Fehlerbericht des Kundens, sondern auch wertvolle Informationen, die eine Fehlerbehebung vereinfachen. 

Willkommen im Dschungel

Manche Eigenarten von Javascript erschweren das Debugging und die Fehleranalyse in Produktivumgebungen. Tief verschachtelte Abhängigkeiten, asynchrone Events, der ständig wechselnde Scope im Programmablauf - hier kann man schnell den Überblick verlieren und verfolgt die falsche Fährte. Mit den folgenden grundlegenden Ansätzen kann man etwas Klarheit in diesen Dschungel bringen:

Source Maps - zeigen den Weg zum Quellcode

Der ausgelieferte Code einer Applikation ist für den Browser optimiert und in den meisten Fällen obfuscated, minified und befindet sich in einer einzelnen Zeile und ist daher für den Menschen sehr schwer lesbar. Im Fehlerfall ist die entsprechende Codestelle für den Nutzer verwirrend, aber auch für den Entwickler kaum aussagekräftig.

Beispielcode aus einem ExtJs Production Build: 

 

setX:function(b,a){return this.setXY([b,this.getY()],a)},setXY:function(a,c){var b=this;if(!c||!b.anim){arguments.callee.$previous.call(this,a)}else{if(!Ext.isObject(c)
){c={}Source r Datei kann man den gebuildeten Code wieder lesbar machen.

 

 

Source Maps schaffen hier Abhilfe. Beim Build wird zusätzlich ein Mapping-File erstellt, welches Referenzen von dem Ursprungscode enthält. Mit Hilfe dieser Datei kann man den gebuildeten Code wieder lesbar machen.Datei kann man den gebuildeten Code wieder lesbar machen.

Beispielsweise kann man mit Sencha Command dies folgendermaßen lösen: 
Mit Sencha Command muss man zunächst einen normalen Build erstellen, bei dem der Code der App selbst nur in einer Datei app-concatenated.js lesbar zusammengefasst und nicht minified wird. Dann lässt sich die Source Map im App-Verzeichnis mit dem Aufruf

 

Sencha fs minify -f=build/app-concatenated.js -t=build/app.minified.js -generate-source-map -closure

 

erzeugen. Die Datei app.minified.js enthält dann den minimierten Code für die Produktivumgebung. Das -closure Flag zeigt, dass Sencha Command hierfür Google Closure verwendet.

Die erhaltende Source Map Datei app.js.map lässt sich dann im Browser verwenden, um im Debugger den Code lesbar darzustellen. Als Beispiel muss im Chrome Browser in Settings unter Preferences > Sources der Schalter Enable JavaScript source maps aktiviert sein. Die Datei app-concatenated.js, enthält den lesbaren kommentierten Code. Dabei ist zu beachten, dass diese in der Live Umgebung zugriffsgeschützt ist. Source Maps sind eine wichtige Grundlage für jede weitere Fehleranalyse, da sie den Quellcode wieder lesbar machen. Die problematische Codestelle selbst kann man mit Hilfe des Callstacks eingrenzen.

Stacktrace - ein Haufen Hinweise

Der Stacktrace, ein Ausschnitt aus dem Callstack des Browsers, ist sehr hilfreich bei der Fehlersuche. Denn darin befinden sich die letzten Funktionsaufrufe bis zum Zeitpunkt, wo der Fehler passiert. In allen neueren Browsern* findet man den Stacktrace in der stack-Property der Exception.

Zugriff auf den Stacktrace einer Exception

 

try {
    throw new Error('Bug!');
}
catch(e) {
    console.log(e.stack);
}

 

Logging - Nichts wegwerfen

Wie kommt der Stacktrace nun zum Entwickler? Hier hilft ein globaler Error Handler. Der Listener behandelt global alle auftretenden Fehler, die nicht per try...catch abgefangen wurden. Hat man also einen Fehlerfall nicht bedacht oder per try...catch abgefangen, lässt sich trotzdem feststellen, dass etwas nicht funktioniert hat.

Wird ein Fehler also nicht behandelt, können wir Fehler und eventuelle Meta-Informationen an einen Server senden.

Callstack auf dem Server speichern

 

window.addEventListener('error', function (event) {
    var stack = event.error.stack,
        message = event.error.toString(),
        xhr = new XMLHttpRequest();

    if (stack) {
        message += '\n' + stack;
    }
    // Fehlermeldung und Stacktrace an Server schicken zur weiteren Protokollierung
    xhr.open('POST', '/log', true);
    xhr.send(message);
});

 

In manchen extremen Fehlerfällen kann es vorkommen, dass der Browser Requests nicht mehr absetzt und somit ist ein direktes Speichern per AJAX nicht mehr möglich. Hier gibt es dennoch die Möglichkeit mit einem Trick den Fehler zu loggen. Wenn nach einem gesetzten Timeout keine Antwort auf den Request kommt, wird ein Bild vom Server angefragt. Der Pfad der Anfrage ist hier eigentlich der Text der Fehlermeldung. Der Server registriert also einen Zugriff auf eine nicht existente Datei und protokoliert dies in seinen Logdateien. Damit ist der Fehler festgehalten und kann serverseitig eingesehen werden.

Einfaches Loggen bei XHR Ausfall

 

window.addEventListener('error', function (event) {
    [...]
    // Code wie oben, zusätzlich nach erfolglosem AJAX Request die Meldung als Zugriff 'übergeben'
    xhr.timeout = 2000;//2 Sekunden Timeout
    xhr.ontimeout = function() {
        // Bild vom Server anfordern
        new Image().src = '/log' + '?errorLog=' + encodeURIComponent(message);
    };
    xhr.send(message);
});

 

Einsatzbereit?

Leider ist "Error.stack" nicht Teil des JavaScript Standards und funktioniert somit nicht in jedem Browser und jeder Version einheitlich.
Um Fehler, zum Beispiel zum Aufspüren von Duplikaten, vergleichen zu können, muss der Stacktrace vor dem Abschicken normalisiert werden.
Hier bieten sich Bibliotheken wie StackTrace.JS an, die für alle unterstützten Browser einen einheitlich formatierten Stacktrace liefern und auf Wunsch auch gleich eine Source Map anwenden.

Echte (Er)Lösung in Sicht

Dies ist nur ein kleiner Einblick über den Aufwand, den man für clientseitige Fehleranalysen und Monitoring betreiben kann. Doch dies reicht, um einen Eindruck in die Komplexität des Themas zu geben. Durch die Vielfalt der Browser, kann man als Entwickler im clientseitigen JavaScript niemals 100% fehlersicher sein.
Wir stellen in den nächsten Teilen dieser Blogartikel-Reihe zwei Werkzeuge für den produktiven Einsatz vor, die genau für diese Aufgabe entwickelt wurden:

Sentry von Functional Software, Inc
RootCause von Bryntum

* Chrome, Edge, Firefox (Gecko) 30, Internet Explorer 10, Opera, Safari 6

Quellen:
A Guide to Proper Error Handling in JavaScript
MDN