Ausnahmebehandlung in JavaScript

Axel Rauschmayer
Ausnahmen (engl. exceptions) sind ein Mechanismus zur Fehlerbehandlung. Er erlaubt es, Fehler, die tief verschachtelt in Funktionsaufrufen stattfinden, auf einer höheren Ebene zu behandeln, da man dort im Allgemeinen besser reagieren kann. Der Artikel behandelt auch den Einsatz von sogenannten Stack-Traces bei der Fehlersuche. Diese sind eine genaue Angabe, welche Funktionsaufrufe beim Auftreten eines Fehlers aktiv waren. Abschließend werden die eingebauten Typen für Ausnahmen besprochen und wie man sie für eigene Zwecke anpasst.

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:

  1. Es findet ein Fehler statt, der vor Ort nicht behandelt werden kann – wirf eine Ausnahme (**).
  2. Finde eine Stelle, an der Fehler sinnvoll behandelt werden können – fange Ausnahmen (*).
An der Stelle (**) sind gerade folgende Konstrukte aktiv:

    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 von throw 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:

  1. Es kann ein externer Fehler aufgetreten sein – der Benutzer hat eine falsche Eingabe gemacht, im System ist eine Datei nicht lesbar etc.
  2. 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:

  1. Daten: Die Werte aller Variablen (global, lokal, Parameter).
  2. 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 mittels instanceof ü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 von eval() 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
    
Propertys der Instanzen von Error (bzw. eines Untertypen):
  • message: Fehlermeldung
  • name: Name des Fehlers
  • stack: 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 von x und y abgeleitet.
  • Aufruf als Funktion: Constr.call(obj, x, y). Constr fügt dem existierenden Objekt obj 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,stack
Beachten 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

  1. stacktrace.js: Eine Bibliothek zur Erzeugung von Stack-Traces auf allen JavaScript-Plattformen.
Axel Rauschmayer Dr. Axel Rauschmayer ist Consultant und Trainer für JavaScript, Web-Technologien und Informationsmanagement. Er entwickelt seit 1995 Web-Anwendungen und hielt seinen ersten Vortrag über Ajax im Jahre 2006. 1999 war er technischer Manager des Internet-Startups Pangora, das später europaweit expandierte.

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.