Wenn wir etwas entwickeln, benötigen wir oft unsere eigenen Fehlerklassen, um bestimmte Dinge widerzuspiegeln, die bei unseren Aufgaben schiefgehen können. Für Fehler bei Netzwerkoperationen benötigen wir möglicherweise HttpError
, für Datenbankoperationen DbError
, für Suchoperationen NotFoundError
und so weiter.
Unsere Fehler sollten grundlegende Fehlereigenschaften wie message
, name
und vorzugsweise stack
unterstützen. Sie können aber auch andere eigene Eigenschaften haben, z. B. können HttpError
Objekte eine statusCode
Eigenschaft mit einem Wert wie 404
oder 403
oder 500
haben.
JavaScript ermöglicht die Verwendung throw
mit jedem Argument, sodass unsere benutzerdefinierten Fehlerklassen technisch gesehen nicht von Error
erben müssen. Aber wenn wir erben, wird es möglich obj instanceof Error
zu verwenden, um Fehlerobjekte zu identifizieren. Es ist also besser, davon zu erben.
Wenn die Anwendung wächst, bilden unsere eigenen Fehler natürlich eine Hierarchie. Beispielsweise kann HttpTimeoutError
von HttpError
usw. erben.
Betrachten wir als Beispiel eine Funktion readUser(json)
, die JSON mit Benutzerdaten lesen soll.
Hier ist ein Beispiel dafür, wie ein gültiger json
aussehen kann:
let json = `{ "name": "John", "age": 30 }`;
Intern verwenden wir JSON.parse
. Wenn ein fehlerhafter json
empfangen wird, wird SyntaxError
ausgegeben. Aber selbst wenn json
syntaktisch korrekt ist, heißt das nicht, dass es sich um einen gültigen Benutzer handelt, oder? Möglicherweise fehlen die erforderlichen Daten. Beispielsweise verfügt es möglicherweise nicht über name
und age
, die für unsere Benutzer wichtig sind.
Unsere Funktion readUser(json)
liest nicht nur JSON, sondern überprüft („validiert“) die Daten. Wenn keine Pflichtfelder vorhanden sind oder das Format falsch ist, liegt ein Fehler vor. Und das ist kein SyntaxError
, weil die Daten syntaktisch korrekt sind, sondern eine andere Art von Fehler. Wir nennen es ValidationError
und erstellen eine Klasse dafür. Ein Fehler dieser Art sollte auch Informationen über das fehlerhafte Feld enthalten.
Unsere ValidationError
-Klasse sollte von der Error
-Klasse erben.
Die Error
-Klasse ist integriert, aber hier ist ihr ungefährer Code, damit wir verstehen, was wir erweitern:
// Der „Pseudocode“ für die integrierte Error-Klasse, die von JavaScript selbst definiert wird Klassenfehler { Konstruktor(Nachricht) { this.message = Nachricht; this.name = "Fehler"; // (verschiedene Namen für verschiedene integrierte Fehlerklassen) this.stack = <Aufrufstapel>; // Nicht standardmäßig, aber die meisten Umgebungen unterstützen es } }
Lassen Sie uns nun ValidationError
davon erben und es in Aktion ausprobieren:
Klasse ValidationError erweitert Error { Konstruktor(Nachricht) { super(Nachricht); // (1) this.name = "ValidationError"; // (2) } } Funktionstest() { throw new ValidationError("Whoops!"); } versuchen { prüfen(); } Catch(err) { alarm(err.message); // Hoppla! alarm(err.name); // ValidationError alarm(err.stack); // eine Liste verschachtelter Aufrufe mit jeweiligen Leitungsnummern }
Bitte beachten Sie: In Zeile (1)
rufen wir den übergeordneten Konstruktor auf. JavaScript erfordert, dass wir super
im untergeordneten Konstruktor aufrufen, das ist also obligatorisch. Der übergeordnete Konstruktor legt die message
fest.
Der übergeordnete Konstruktor setzt auch die name
auf "Error"
, daher setzen wir sie in Zeile (2)
auf den richtigen Wert zurück.
Versuchen wir es in readUser(json)
zu verwenden:
Klasse ValidationError erweitert Error { Konstruktor(Nachricht) { super(Nachricht); this.name = "ValidationError"; } } // Nutzung Funktion readUser(json) { let user = JSON.parse(json); if (!user.age) { throw new ValidationError("Kein Feld: Alter"); } if (!user.name) { throw new ValidationError("Kein Feld: Name"); } Benutzer zurückgeben; } // Arbeitsbeispiel mit try..catch versuchen { let user = readUser('{ "age": 25 }'); } Catch (Err) { if (err Instanz von ValidationError) { Alert("Ungültige Daten: " + err.message); // Ungültige Daten: Kein Feld: Name } else if (err Instanz von SyntaxError) { // (*) alarm("JSON-Syntaxfehler: " + err.message); } anders { wirf irr; // unbekannter Fehler, erneut auslösen (**) } }
Der try..catch
Block im obigen Code verarbeitet sowohl unseren ValidationError
als auch den integrierten SyntaxError
von JSON.parse
.
Sehen Sie sich bitte an, wie wir mit instanceof
nach dem spezifischen Fehlertyp in der Zeile (*)
suchen.
Wir könnten uns err.name
auch so ansehen:
// ... // statt (err Instanz von SyntaxError) } else if (err.name == "SyntaxError") { // (*) // ...
Die Version instanceof
ist viel besser, da wir in Zukunft ValidationError
erweitern und Untertypen davon erstellen werden, z. B. PropertyRequiredError
“. Und instanceof
Instanzprüfung funktioniert weiterhin für neue erbende Klassen. Das ist also zukunftssicher.
Außerdem ist es wichtig, dass, wenn catch
auf einen unbekannten Fehler stößt, dieser erneut in die Zeile (**)
eingefügt wird. Der catch
-Block kann nur mit Validierungs- und Syntaxfehlern umgehen, andere Arten (verursacht durch einen Tippfehler im Code oder andere unbekannte Gründe) sollten durchfallen.
Die ValidationError
-Klasse ist sehr allgemein gehalten. Viele Dinge können schief gehen. Die Eigenschaft fehlt möglicherweise oder hat ein falsches Format (z. B. ein Zeichenfolgewert für age
anstelle einer Zahl). Lassen Sie uns eine konkretere Klasse PropertyRequiredError
erstellen, genau für fehlende Eigenschaften. Es enthält zusätzliche Informationen über die fehlende Immobilie.
Klasse ValidationError erweitert Error { Konstruktor(Nachricht) { super(Nachricht); this.name = "ValidationError"; } } Klasse PropertyRequiredError erweitert ValidationError { Konstruktor(Eigenschaft) { super("Keine Eigenschaft: " + Eigenschaft); this.name = "PropertyRequiredError"; this.property = Eigenschaft; } } // Verwendung Funktion readUser(json) { let user = JSON.parse(json); if (!user.age) { throw new PropertyRequiredError("age"); } if (!user.name) { throw new PropertyRequiredError("name"); } Benutzer zurückgeben; } // Arbeitsbeispiel mit try..catch versuchen { let user = readUser('{ "age": 25 }'); } Catch (Err) { if (err Instanz von ValidationError) { Alert("Ungültige Daten: " + err.message); // Ungültige Daten: Keine Eigenschaft: Name alarm(err.name); // PropertyRequiredError alarm(err.property); // Name } else if (err Instanz von SyntaxError) { alarm("JSON-Syntaxfehler: " + err.message); } anders { wirf irr; // unbekannter Fehler, erneut auslösen } }
Die neue Klasse PropertyRequiredError
ist einfach zu verwenden: Wir müssen nur den Eigenschaftsnamen übergeben: new PropertyRequiredError(property)
. Die für Menschen lesbare message
wird vom Konstruktor generiert.
Bitte beachten Sie, dass this.name
im PropertyRequiredError
-Konstruktor erneut manuell zugewiesen wird. Das kann etwas mühsam werden – this.name = <class name>
in jeder benutzerdefinierten Fehlerklasse zuzuweisen. Wir können dies vermeiden, indem wir unsere eigene „Basisfehler“-Klasse erstellen, die this.name = this.constructor.name
zuweist. Und dann alle unsere benutzerdefinierten Fehler davon erben.
Nennen wir es MyError
.
Hier ist der Code mit MyError
und anderen benutzerdefinierten Fehlerklassen, vereinfacht:
Klasse MyError erweitert Error { Konstruktor(Nachricht) { super(Nachricht); this.name = this.constructor.name; } } Klasse ValidationError erweitert MyError { } Klasse PropertyRequiredError erweitert ValidationError { Konstruktor(Eigenschaft) { super("Keine Eigenschaft: " + Eigenschaft); this.property = Eigenschaft; } } // Name ist korrekt alarm( new PropertyRequiredError("field").name ); // PropertyRequiredError
Jetzt sind benutzerdefinierte Fehler viel kürzer, insbesondere ValidationError
, da wir die Zeile "this.name = ..."
im Konstruktor entfernt haben.
Der Zweck der Funktion readUser
im obigen Code besteht darin, „die Benutzerdaten zu lesen“. Dabei können verschiedene Arten von Fehlern auftreten. Im Moment haben wir SyntaxError
und ValidationError
, aber in Zukunft könnte die readUser
-Funktion wachsen und wahrscheinlich andere Arten von Fehlern erzeugen.
Der Code, der readUser
aufruft, sollte diese Fehler behandeln. Im Moment verwendet es mehrere if
s im catch
-Block, die die Klasse überprüfen, bekannte Fehler behandeln und unbekannte erneut auslösen.
Das Schema sieht so aus:
versuchen { ... readUser() // die potenzielle Fehlerquelle ... } Catch (Err) { if (err Instanz von ValidationError) { // Validierungsfehler behandeln } else if (err Instanz von SyntaxError) { // Syntaxfehler behandeln } anders { wirf irr; // unbekannter Fehler, erneut auslösen } }
Im obigen Code sehen wir zwei Arten von Fehlern, es können aber noch mehr sein.
Wenn die readUser
-Funktion mehrere Arten von Fehlern generiert, sollten wir uns fragen: Wollen wir wirklich jedes Mal einzeln nach allen Fehlertypen suchen?
Oft lautet die Antwort „Nein“: Wir möchten „eine Ebene über all dem“ sein. Wir wollen nur wissen, ob ein „Datenlesefehler“ vorliegt – warum genau das passiert ist, ist oft unerheblich (die Fehlermeldung beschreibt es). Oder, noch besser, wir hätten gerne eine Möglichkeit, die Fehlerdetails abzurufen, aber nur, wenn es nötig ist.
Die Technik, die wir hier beschreiben, wird „Wrapping Exceptions“ genannt.
Wir erstellen eine neue Klasse ReadError
, um einen generischen „Datenlesefehler“ darzustellen.
Die Funktion readUser
fängt darin auftretende Datenlesefehler ab, wie z. B. ValidationError
und SyntaxError
, und generiert stattdessen einen ReadError
.
Das ReadError
Objekt behält den Verweis auf den ursprünglichen Fehler in seiner cause
Eigenschaft.
Dann muss der Code, der readUser
aufruft, nur auf ReadError
prüfen, nicht auf jede Art von Datenlesefehlern. Und wenn weitere Details zu einem Fehler benötigt werden, kann die Eigenschaft cause
überprüft werden.
Hier ist der Code, der ReadError
definiert und seine Verwendung in readUser
und try..catch
demonstriert:
Klasse ReadError erweitert Error { Konstruktor(Nachricht, Ursache) { super(Nachricht); this.cause = Ursache; this.name = 'ReadError'; } } Klasse ValidationError erweitert Fehler { /*...*/ } Klasse PropertyRequiredError erweitert ValidationError { /* ... */ } Funktion validierenBenutzer(Benutzer) { if (!user.age) { throw new PropertyRequiredError("age"); } if (!user.name) { throw new PropertyRequiredError("name"); } } Funktion readUser(json) { Benutzer lassen; versuchen { user = JSON.parse(json); } Catch (Err) { if (err Instanz von SyntaxError) { throw new ReadError("Syntaxfehler", err); } anders { wirf irr; } } versuchen { validierenBenutzer(Benutzer); } Catch (Err) { if (err Instanz von ValidationError) { throw new ReadError("Validation Error", err); } anders { wirf irr; } } } versuchen { readUser('{bad json}'); } fangen (e) { if (e Instanz von ReadError) { Warnung(e); // Ursprünglicher Fehler: SyntaxError: Unerwartetes Token b in JSON an Position 1 alarm("Ursprünglicher Fehler: " + e.cause); } anders { wirf e; } }
Im obigen Code funktioniert readUser
genau wie beschrieben – es fängt Syntax- und Validierungsfehler ab und löst stattdessen ReadError
Fehler aus (unbekannte Fehler werden wie üblich erneut ausgelöst).
Der äußere Code überprüft also instanceof ReadError
und das war’s. Es ist nicht erforderlich, alle möglichen Fehlertypen aufzulisten.
Der Ansatz wird „Wrapping-Ausnahmen“ genannt, weil wir „Low-Level“-Ausnahmen nehmen und sie in ReadError
„verpacken“, was abstrakter ist. Es wird häufig in der objektorientierten Programmierung verwendet.
Wir können normalerweise von Error
und anderen integrierten Fehlerklassen erben. Wir müssen uns nur um die name
kümmern und nicht vergessen, super
aufzurufen.
Wir können instanceof
verwenden, um nach bestimmten Fehlern zu suchen. Es funktioniert auch mit der Vererbung. Aber manchmal haben wir ein Fehlerobjekt, das aus einer Bibliothek eines Drittanbieters stammt, und es gibt keine einfache Möglichkeit, seine Klasse abzurufen. Dann kann für solche Prüfungen name
verwendet werden.
Das Einschließen von Ausnahmen ist eine weit verbreitete Technik: Eine Funktion behandelt Ausnahmen auf niedriger Ebene und erzeugt Fehler auf höherer Ebene anstelle verschiedener Fehler auf niedriger Ebene. Ausnahmen auf niedriger Ebene werden manchmal zu Eigenschaften dieses Objekts wie err.cause
in den obigen Beispielen, aber das ist nicht unbedingt erforderlich.
Wichtigkeit: 5
Erstellen Sie eine Klasse FormatError
, die von der integrierten Klasse SyntaxError
erbt.
Es sollte message
, name
und stack
unterstützen.
Anwendungsbeispiel:
let err = new FormatError("Formatierungsfehler"); alarm( err.message ); // Formatierungsfehler alarm( err.name ); // FormatError alarm( err.stack ); // Stapel Alert( err Instanz von FormatError ); // WAHR Alert( err Instanz von SyntaxError ); // true (weil von SyntaxError erbt)
Klasse FormatError erweitert SyntaxError { Konstruktor(Nachricht) { super(Nachricht); this.name = this.constructor.name; } } let err = new FormatError("Formatierungsfehler"); alarm( err.message ); // Formatierungsfehler alarm( err.name ); // FormatError alarm( err.stack ); // Stapel Alert( err Instanz von SyntaxError ); // WAHR