Ausnahmebehandlung in JavaScript
Was sind Ausnahmen?
Ausnahmen sind eine Technik, um auftretende Fehler zu behandeln. Sie lösen folgendes Grundproblem: Wenn ein Algorithmus in mehrere Schritte aufgeteilt ist, die durch Funktionen (inkl. Methoden) implementiert sind, so rufen diese Funktionen meist weitere Funktionen auf (etc.). Fehler finden üblicherweise auf einer tiefen Aufrufebene statt, können aber am besten auf einer höheren Ebene behandelt werden, indem z.B. ein Schritt übersprungen oder wiederholt wird. Dazu ein Beispiel: Gegeben sei ein Algorithmus sammleInformation
, der in drei Schritte aufgeteilt ist.
- Schritt 1: Dateinamen aus einer Datei lesen.
- Schritt 2: Über die Dateien iterieren und aus jeder Datei Information sammeln.
- Schritt 3: Die gesammelte Information in eine Datei schreiben.
In Code sieht das so aus:
function sammleInformation() { var dateiNamen = schritt1(); var info = schritt2(dateiNamen); schritt3(info); } function schritt2(dateiNamen) { for(var i=0; i<dateiNamen.length; i++) { verarbeiteDatei(dateiNamen[i]); (*) } } function verarbeiteDatei(dateiName) { var datei = openFile(dateiName); // (**) // ... } // Nicht gezeigt: schritt1() und schritt2()
Wie geht man nun am besten damit um, wenn an der Stelle (**) etwas unvorhergesehenes, eine Ausnahme passiert? An Ort und Stelle ist man machtlos. Am einfachsten ist es, die momentane Datei zu überspringen und die nächste Datei zu bearbeiten, an der Stelle (*). Mit Ausnahmen lässt sich das wie folgt lösen (sammleInformationen()
wird nicht mehr gezeigt):
function schritt2(dateiNamen) { for (var i=0; i<dateiNamen.length; i++) { try { verarbeiteDatei(dateiNamen[i]); } catch (ausnahme) { // (*) alert("Fehler beim Verarbeiten der Datei "+dateiName[i]); } } } function verarbeiteDatei(dateiName) { var datei = openFile(dateiName); // ... } function openFile(fileName) { if (!exists(fileName)) { throw new Error("Could not find file "+fileName); // (**) } }
Die Ausnahmebehandlung findet in zwei Schritten statt:
- Es findet ein Fehler statt, der vor Ort nicht behandelt werden kann – wirf eine Ausnahme (**).
- Finde eine Stelle, an der Fehler sinnvoll behandelt werden können – fange Ausnahmen (*).
sammleInformation() schritt2(...) try { ... } catch (wert) { ... } verarbeiteDatei(...) openFile(...) throw wert
Die throw
-Anweisung arbeitet sich so lange durch diese geschachtelten Ebenen nach oben, bis sie einen try...catch
-Block findet. Sie kehrt dann dorthin zurück und übergibt einen Wert. Dieser Wert ist die Ausnahme, die geworfen wurde (engl. throw an exception). Im Folgenden werden die Details dieser Mechanismen genauer besprochen.
Ausnahmebehandlung in JavaScript
Ausnahmebehandlung in JavaScript funktioniert ähnlich wie in den meisten Programmiersprachen: Wenn eine Ausnahme geworfen wird, dann werden so lange aktive Funktionsaufrufe (inkl. Methodenaufrufe) verlassen, bis man innerhalb eines try
-Blocks ist, der eine angehängte catch
-Klausel hat. In letzterer wird die Ausführung fortgesetzt, sie erhält den geworfenen Wert als Argument.
throw
Die Syntax vonthrow
ist wie folgt:
throw wert;
Jede Art von JavaScript-Wert kann geworfen werden. Aus Gründen der Einfachheit werfen viele JavaScript-Programme Zeichenketten (z.B. throw "Fehler!"
). Aber JavaScript hat auch spezialisierte Typen für diesen Zweck: Error
und dessen Untertypen (siehe unten). Die wichtigste Funktionalität dieser Typen ist, dass Instanzen einen Stack-Trace vom Zeitpunkt ihrer Erschaffung enthalten. Dieser hilft einem dabei, herauszufinden, wo die Ausnahme auftrat, denn er gibt an, welche Funktionsaufrufe aktiv waren (siehe unten).
try...catch...finally
Die Syntax von try...catch...finally
ist wie folgt. Der try
-Block muss immer präsent sein und mindestens einer der Blöcke catch
und finally
muss folgen.
try { try-Anweisungen } [catch (parameter) { catch-Anweisungen }] [finally { finally-Anweisungen }]
catch
fängt jede Ausnahme, die in den try-Anweisungen geworfen wird, sei es direkt oder in einer Funktion, die aufgerufen wird. Das folgende Beispiel zeigt, dass jede Art von Wert geworfen und gefangen werden kann.
function versuche(versuch) { try { versuch(); } catch (e) { console.log("Caught: "+e); } }
Interaktion:
> versuche(function() { throw 3 }); Caught: 3 > versuche(function() { throw "Es gibt ein Problem!" }); Caught: Es gibt ein Problem! > versuche(function() { throw new Error("Es gibt ein Problem!") }); Caught: Error: Es gibt ein Problem!
Error
ist einer der Standardtypen für Ausnahmen, die weiter unten besprochen werden.
finally
wird immer ausgeführt, egal was in den try-Anweisungen passiert. Dies wird meist dazu verwendet, um Aufräumarbeiten zu erledigen, die in jedem Fall nötig sind. Öffnet man z.B. eine Datei, so will man sie wieder schließen, auch wenn in einer Anweisung danach eine Ausnahme auftritt. Packt man die kritischen Anweisungen in einen try
-Block, so kann man sich davor schützen, dass die momentane Funktion vorschnell durch eine Ausnahme verlassen wird, indem man das Schließen der Datei im finally
-Block vornimmt. Beispiel:
function foo() { var f = openFile(); try { // ... throwsError(); // ... } finally { f.close(); } } function throwsError() { throw new Error("Sorry..."); }
Unerwartete Ausnahmen und Stack-Traces
Ausnahmen weisen auf zwei Arten von nicht-regelmäßigen Ereignissen hin:
- Es kann ein externer Fehler aufgetreten sein – der Benutzer hat eine falsche Eingabe gemacht, im System ist eine Datei nicht lesbar etc.
- Es kann ein interner Fehler aufgetreten sein – das Programm hat einen falschen Parameter angegeben oder anderweitig unerlaubte Dinge getan.
Ausnahmen sind im zweiten Fall immer ungewollt und deuten auf einen Fehler im Programm hin. Im ersten Fall kann die Ausnahme auch manchmal Schuld des Programms sein. Das heißt für den Programmierer, dass er sich auf die Fehlersuche machen muss. Beim Entwickeln hat man manchmal noch den Komfort eines Debuggers auf seiner Seite. Doch spätestens im normalen Einsatz eines Programms kann man sich auch darauf nicht mehr verlassen. Für eine effiziente Fehlersuche muss man möglichst viel über die Umstände erfahren, unter denen eine Ausnahme geworfen wurde. Sprich: man will möglichst genau wissen, in welchem Zustand sich das Programm befand. Dieser Zustand hat zwei Komponenten:
- Daten: Die Werte aller Variablen (global, lokal, Parameter).
- Ausführung: In welcher Zeile fand die Ausnahme statt, welche Funktionsaufrufe waren aktiv?
(1) kann man teilweise in die Fehlermeldung der Ausnahme packen. (2) wird von einigen JavaScript-Interpretern unterstützt, indem sie Stack-Traces in Ausnahmen speichern. Der Name Stack-Trace besagt, dass eine Momentaufnahme (engl. trace bedeutet Linie, Spur, Protokoll) des Stacks gemacht wird. Der Stack (deutsch auch Stapel oder Keller genannt) ist die Datenstruktur, die zum Verwalten von Funktionsaufrufen eingesetzt wird. Ein Stack-Trace kann z.B. folgende Gestalt haben:
sort (util.js:119) sortHelper (helpers.js:12) main (myapp.js:330)
Das bedeutet: Als der Stack-Trace erzeugt wurde, befand sich die Ausführung in der Funktion sort
, in der Datei util.js
in der Zeile 119. Die Funktion sort
wurde von der Funktion sortHelper
aufgerufen, die wiederum von der Funktion main
aufgerufen wurde. Man merkt schon, dass man ein gutes Bild davon bekommt, unter welchen Umständen der Stack-Trace erzeugt wurde. Zur Entwicklungszeit wird der Stack-Trace meist auf dem Bildschirm ausgegeben, sei es vom Programm oder vom JavaScript-Interpreter. Zur Einsatzzeit hat man die Möglichkeit, Stack-Traces samt Fehlermeldungen in einer Datei mitzuschreiben und sie später bei der Fehlersuche einzusetzen.
Beispiel: Node.js speichert einen Stack-Trace im Property stack
, wenn man ein Objekt wirft, das eine Instanz von Error
ist (Details zu Error
– siehe nächster Abschnitt).
function fange() { try { werfe(); } catch(e) { console.log(e.stack); // gib Stack-Trace aus } } function werfe() { throw new Error("Es ist ein Fehler aufgetreten"); }Ausführung:
> fange() Error: Es ist ein Fehler aufgetreten at werfe ([object Context]:2:7) at fange ([object Context]:3:1) ...
Leider unterstützen nicht alle JavaScript-Interpreter das Property stack
. Die Bibliothek stacktrace.js [1] bietet eine plattformübergreifende Lösung zum Erzeugen von Stack-Traces (entweder vom aktuellen Stack oder als Extrakt einer Error-Instanz).
Standardtypen für Ausnahmen
Wir haben bereits gesehen, dass in JavaScript beliebige Werte geworfen werden können. JavaScript hat jedoch auch spezielle Typen für Ausnahmen – Error
und dessen Untertypen. Einen dieser Typen zu verwenden hat mehrere Vorteile:
- Instanzen sind Objekte: Damit kann man einer Ausnahme beliebig Propertys für eigene Zwecke hinzufügen. Diese Möglichkeit hat man bei einem primitiven Wert wie einer Zeichenkette nicht.
- Der Ausnahme-Typ gibt die Ursache an: Am Typ einer Ausnahme kann man erkennen, aus welchem Grund sie geworfen wurde. So werden z.B. Instanzen des Typs
RangeError
geworfen, wenn ein numerischer Wert nicht innerhalb des zulässigen Bereichs (engl. range) ist. Die Ursache lässt sich also mittelsinstanceof
überprüfen. - Eingebaute Unterstützung für Stack-Traces: In JavaScript-Interpretern, die Stack-Traces unterstützen, erhalten
Error
-Objekte bei der Instanziierung automatisch einen Trace. Naturgemäß hilft das dabei, herauszufinden, unter welchen Umständen die Ausnahme auftrat.
Die folgenden Typen sind von ECMAScript zum Zweck der Ausnahmebehandlung standardisiert worden (ECMAScript-Standard 5.1, §15.11). Sie finden in den eingebauten Funktionen und Typen reichlich Verwendung. Die Namen der Typen lassen erkennen, wie sie eingesetzt werden. Text in Anführungszeichen ist aus dem ECMAScript-Standard übersetzt.
Error
: ein generischer Typ für Fehler. Alle anderen Fehlertypen, die hier erwähnt werden, sind Untertypen dieses Typs.EvalError
: „Diese Ausnahme wird momentan nicht innerhalb dieser Spezifikation verwendet. Sie verbleibt aus Gründen der Kompatibilität mit vorhergehenden Ausgaben dieser Spezifikation.“RangeError
: „weist darauf hin, dass sich ein numerischer Wert außerhalb des zulässigen Bereichs (engl. range) befindet.“ Beispiel:> new Array(-1) RangeError: Invalid array length
ReferenceError
: „weist darauf hin, dass ein unzulässiger Referenzwert entdeckt wurde.“ Meistens handelt es sich hierbei um eine Variable, die es nicht gibt. Beispiel:> unkownVariable ReferenceError: unkownVariable is not defined
SyntaxError
: „weist darauf hin, dass ein Parsefehler aufgetreten ist.“ Zum Beispiel, wenn Code voneval()
geparst wird:> eval("3 +") SyntaxError: Unexpected end of file
TypeError
: „weist darauf hin, dass der tatsächliche Typ eines Operanden nicht der erwartete Typ ist.“ Beispiel:> "abc"() TypeError: string is not a function
URIError
: „weist darauf hin, dass eine der globalen Funktionen, die mit URIs umgeht, auf eine Weise verwendet wurde, die nicht zu ihrer Definition passt.“ Beispiel:> decodeURI("%2") URIError: URI malformed
message
: Fehlermeldungname
: Name des Fehlersstack
: Ein Stack-Trace. Nicht standardisiert, aber in vielen JavaScript-Implementierungen verfügbar, z.B. Chrome, Node.js und Firefox.
Eigene Untertypen von Error schreiben
Für die eigene Fehlerbehandlung kann man auf die eingebauten Fehlertypen zurückgreifen und bekommt damit automatisch Stack-Traces. Es kann aber auch sein, dass einem die Standard-Möglichkeiten nicht genügen und man gerne weitere Typen hinzufügen würde – mit aussagekräftigen Namen für Fehlergründe. Das macht einem JavaScript leider nicht einfach: Der Konstruktor Error
legt immer eine neue Instanz an, selbst wenn er als Funktion aufgerufen wird. Normalerweise gibt es zwei Modi, mit denen ein Konstruktor Constr
aufgerufen wird:
- Aufruf als Konstruktor:
new Constr(x, y)
. Eine neue Instanz wird erzeugt,Constr
fügt dieser seine Propertys hinzu. Diese werden unter anderem vonx
undy
abgeleitet. - Aufruf als Funktion:
Constr.call(obj, x, y)
.Constr
fügt dem existierenden Objektobj
seine Propertys hinzu.
Letzteres kann man bei Error
samt Untertypen nicht. Man benötigt es aber zwingend, um einen Untertypen zu schreiben, denn auf diesem Weg werden einer Unter-Instanz die Propertys des Obertypen hinzugefügt. Man muss also in Error
-Untertypen zu einem Trick greifen: Zuerst erzeugt new
automatisch eine Unterinstanz. Dann legt man im Konstruktor eine komplett neue Instanz von Error
(dem Obertypen) an. Abschließend werden alle Propertys der Oberinstanz in this
(die Unterinstanz) kopiert. Das folgende Beispiel demonstriert diesen Trick anhand des Untertyps MyError
.
function MyError() { // Wir benötigen apply(), um arguments weiterzureichen, // also setzen wir Error als Funktion ein. var oberInstanz = Error.apply(null, arguments); copyOwnPropertiesTo(oberInstanz, this); } MyError.prototype = Object.create(Error.prototype); MyError.prototype.constructor = MyError; // Hilfsfunktion function copyOwnPropertiesTo(source, target) { Object.getOwnPropertyNames(source).forEach(function(propName) { Object.defineProperty(target, propName, Object.getOwnPropertyDescriptor(source, propName)); }); return target; }
Als nächstes probieren wir den neuen Fehlertypen aus:
try { throw new MyError("Es ist etwas passiert"); } catch (e) { console.log("KEYS: "+Object.keys(e)); }Ausgabe unter Firefox:
KEYS: message,fileName,lineNumber,stackBeachten Sie, dass das Erstellen von Ausnahmen-Untertypen in JavaScript weit weniger wichtig ist, als in statischen Programmiersprachen, weil man immer
Error
instanziieren und alle benötigten Propertys hinzufügen kann.
Fazit
JavaScript lässt einem bei der Ausnahmebehandlung sehr viele Freiheiten. Oft muss man nur wenig Aufwand treiben; so ist das Hinzufügen eigener Propertys zu einer simplen Error
-Instanz eine echte Alternative zum Anlegen eigener Untertypen. Sollte Letzteres doch einmal nötig sein, können Sie in diesem Artikel nachschlagen, wie es geht.
Referenzen
- stacktrace.js: Eine Bibliothek zur Erzeugung von Stack-Traces auf allen JavaScript-Plattformen.
Aktivitäten: Axel Rauschmayer bloggt auf 2ality.com, tweetet als @rauschma, co-organisiert MunichJS (die JavaScript User Group München), ist Mitbetreiber der JavaScript-Website JS Central und Chefredakteur von mag.js.