Werfen und eigene Exceptions
Kurz gesagt: Mit throw lehnt eine Funktion ab und wirft einen Fehler, der durch die Aufrufe fällt, bis ein catch ihn fängt; für deinen eigenen Fall schreibst du eine eigene Fehlerklasse.
Problem
Der Geldautomat sagt „Nimm dein Geld“
Das Konto speichert seinen Kontostand. Wenn der Betrag zum Abheben negativ ist oder das Geld nicht reicht, tut die Methode einfach nichts:
withdraw(amount) { if (amount > 0 && amount <= this.balance) { this.balance -= amount; }}Der Geldautomat benutzt sie so:
const account = new Account(100);account.withdraw(500);console.log("Nimm dein Geld.");console.log(`Kontostand: ${account.balance} €`);Nimm dein Geld.Kontostand: 100 €Der Kunde will 500 € und hat 100 €. Die Methode lehnt still ab, der Geldautomat merkt nichts und sagt „Nimm dein Geld.“ Der Fehler geht verloren. Die Methode könnte true oder false zurückgeben, aber der Aufrufer vergisst leicht, das zu prüfen, und false sagt nicht, warum.
Die Idee
throw: Ich lehne ab und sage, warum
Statt zu schweigen, wirft die Methode eine Exception:
withdraw(amount) { if (amount <= 0) { throw new RangeError("Der Betrag muss positiv sein"); } if (amount > this.balance) { throw new RangeError("Nicht genug Geld"); } this.balance -= amount;}throw beendet die Methode wie return. Die Zeile this.balance -= amount wird nie erreicht. Aber statt eines Werts schickt die Methode eine Exception, und der Aufrufer kann sie nicht übersehen:
const account = new Account(100);try { account.withdraw(500); console.log("Nimm dein Geld.");} catch (e) { console.log(`Abgelehnt: ${e.message}`);}console.log(`Kontostand: ${account.balance} €`);Abgelehnt: Nicht genug GeldKontostand: 100 €newerzeugt das Exception-Objekt wie jedes andere Objekt. Die Meldung kommt in seinen Konstruktor, unde.messagegibt sie zurück.RangeErrorist die fertige Exception für „der Wert liegt außerhalb des erlaubten Bereichs“. Der Betrag hier ist genau das: unter null oder über dem Kontostand.catch (e)hat in JavaScript keine Art in den Klammern und fängt jede Exception. Hier gibt es nur eine Exception, also reicht das.
Genauer, für Neugierige
- Eine weitere häufige fertige Exception ist
TypeError: Der Wert hat den falschen Typ. Für alles andere nimmst duErroroder eine eigene Exception. thrownimmt in JavaScript jeden Wert an, sogar einen String:throw "Nicht genug Geld"funktioniert. Aber dann hatcatchkeine.messageund keine.stack(beide sindundefined), und es gibt keinen Stacktrace. Wirf deshalb nurErroroder eine Unterklasse davon.
Sieh es dir an
Die Exception fällt durch
withdraw(amount) { if (amount > this.balance) { throw new RangeError("Nicht genug Geld"); } this.balance -= amount;}
function payRent(account) { account.withdraw(800); console.log("Die Miete ist bezahlt.");}
try { payRent(new Account(500));} catch (e) { console.log(`Abgelehnt: ${e.message}`);}Schritt für Schritt, in Worten
- Das Programm ruft payRent für ein Konto mit 500 € auf.
- payRent ruft withdraw(800) auf: ein neuer Frame oben drauf.
- 800 > 500: Die Methode wirft eine Exception.
- Der Kontostand ändert sich nicht: Der Frame wird verworfen, ohne seine Zeilen zu Ende auszuführen.
- payRent hat kein catch: „Die Miete ist bezahlt.“ wird nicht ausgegeben, und der Frame wird verworfen.
- Das catch im Programm fängt die Exception, und das Programm macht hier weiter.
Erinnere dich an den Aufrufstapel aus der Lektion über Rekursion. Ganz unten ist das Programm: der Code auf oberster Ebene. Es ruft payRent auf, und diese Funktion ruft withdraw auf. Die Frames liegen übereinander.
Wenn withdraw wirft, sucht die Exception ein catch, indem sie den Stapel hinuntergeht. Ein Frame ohne passendes catch wird sofort verworfen, ohne seine Zeilen zu Ende auszuführen. So geht es auch payRent. Erst das Programm hat ein catch, und es macht dort weiter.
Gäbe es auch dort kein catch, würde das Programm anhalten und einen Stacktrace ausgeben: genau diese Frames.
Genauer, für Neugierige
Hier ist dasselbe Programm, aber niemand fängt (die Datei rent.js):
class Account { constructor(balance) { this.balance = balance; }
withdraw(amount) { if (amount > this.balance) { throw new RangeError("Nicht genug Geld"); } this.balance -= amount; }}
function payRent(account) { account.withdraw(800); console.log("Die Miete ist bezahlt.");}
payRent(new Account(500));/projects/rent/rent.js:8 throw new RangeError("Nicht genug Geld"); ^
RangeError: Nicht genug Geld at Account.withdraw (/projects/rent/rent.js:8:13) at payRent (/projects/rent/rent.js:15:11) at Object.<anonymous> (/projects/rent/rent.js:19:1) at Module._compile (node:internal/modules/cjs/loader:1705:14) at Object..js (node:internal/modules/cjs/loader:1838:10) at Module.load (node:internal/modules/cjs/loader:1441:32) at Function._load (node:internal/modules/cjs/loader:1263:12) at TracingChannel.traceSync (node:diagnostics_channel:328:14) at wrapModuleLoad (node:internal/modules/cjs/loader:237:24) at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:171:5)
Node.js v22.22.2Node zeigt die Zeile, in der die Exception geworfen wurde, dann die Art und die Meldung und danach die Frames von innen nach außen, wie Java: Account.withdraw, payRent, und Object.<anonymous> ist das Programm. Die Zeilen mit node:internal sind der Code von Node selbst.
Probier es aus
Was erscheint auf dem Bildschirm?
Das Konto hat 50 €, und die Bestellung kostet 120 €.
Was gibt das Programm aus?
function checkout(account) { console.log("Ich bezahle die Bestellung"); account.withdraw(120); console.log("Die Bestellung ist bezahlt");}
const account = new Account(50);try { checkout(account); console.log("Ich sende die Bestätigung");} catch (e) { console.log(`Abgelehnt: ${e.message}`);}console.log(`Kontostand: ${account.balance} €`);„Ich bezahle die Bestellung“ kommt vor dem Abheben. Dann wirft withdraw, und der Frame von checkout wird verworfen: „Die Bestellung ist bezahlt“ erscheint nie. Im Programm wird auch „Ich sende die Bestätigung“ übersprungen, weil es im try nach dem Aufruf steht. catch gibt die Ablehnung aus, und der Kontostand bleibt unberührt.
Probier es aus
Finde den Fehler
Die Miete beträgt 800 €, und auf dem Konto sind 500 €. Das Programm sagt „Die Miete ist bezahlt.“ In welcher Zeile ist der Fehler?
Wähle die Zeile mit dem Fehler.
catch fängt die Exception und tut nichts. Der Fehler verschwindet, genau wie beim Geldautomaten in der ersten Karte. Fangen sollst du dort, wo du etwas Sinnvolles tun kannst. Hier kann payRent das Geld nicht herbeischaffen, also sollte die Funktion nicht fangen. Entferne try/catch: Die Exception erreicht dann das Programm, und dort erfährt die Person davon.Genauer, für Neugierige
- Manchmal fängt eine Funktion, tut etwas, etwa einen Eintrag ins Log schreiben, und wirft dieselbe Exception weiter. In JavaScript ist das
throw e;imcatch. Der Stacktrace bleibt der ursprüngliche: Er wird aufgezeichnet, wenn derErrorerzeugt wird, nicht wenn er geworfen wird.
function payRent(account) { try { account.withdraw(800); } catch (e) { console.log("Log: Die Miete ist nicht durchgegangen"); throw e; }}
try { payRent(new Account(500));} catch (e) { console.log(`Abgelehnt: ${e.message}`);}Log: Die Miete ist nicht durchgegangenAbgelehnt: Nicht genug Geld- Du kannst auch eine andere Exception werfen, die besser sagt, was passiert ist, und die alte als Ursache behalten. In JavaScript:
new Error("Die Miete ist nicht bezahlt", { cause: e }). Die alte steht ine.cause, und Node gibt sie unter der neuen als[cause]aus.
function payRent(account) { try { account.withdraw(800); } catch (e) { throw new Error("Die Miete ist nicht bezahlt", { cause: e }); }}
try { payRent(new Account(500));} catch (e) { console.log(`${e.message}, weil: ${e.cause.message}`);}Die Miete ist nicht bezahlt, weil: Nicht genug GeldDie Idee
Eine eigene Exception
„Nicht genug Geld“ ist Text. Wenn der Geldautomat „Zahle noch 30 € ein“ vorschlagen will, muss er wissen, wie viel fehlt. Die Zahl aus dem Text herauszuziehen ist zerbrechlich. Schreib lieber eine eigene Exception-Klasse mit einem Feld:
class InsufficientFundsError extends Error { constructor(shortfall) { super(`Es fehlen ${shortfall} €`); this.name = "InsufficientFundsError"; this.shortfall = shortfall; }}Deine eigene Exception ist eine gewöhnliche Klasse, die von Error erbt. Der Konstruktor gibt die Meldung mit super(...) an die Basisklasse weiter. this.name gibt der Exception ihren Namen: Ohne ihn sagen e.name und der Stacktrace nur Error. Die Methode wirft sie, wenn das Geld nicht reicht:
withdraw(amount) { if (amount <= 0) { throw new RangeError("Der Betrag muss positiv sein"); } if (amount > this.balance) { throw new InsufficientFundsError(amount - this.balance); } this.balance -= amount;}Und der Aufrufer fängt die beiden Fälle getrennt:
const account = new Account(100);try { account.withdraw(130);} catch (e) { if (e instanceof InsufficientFundsError) { console.log(e.message); console.log(`Zahle noch ${e.shortfall} € ein oder wähle einen kleineren Betrag.`); } else if (e instanceof RangeError) { console.log(`Abgelehnt: ${e.message}`); } else { throw e; }}Es fehlen 30 €Zahle noch 30 € ein oder wähle einen kleineren Betrag.catch gibt es in JavaScript nur einmal, und es fängt alles. Die Art prüfst du darin mit instanceof, und eine Exception, die du nicht erwartest, wirfst du mit throw e; weiter. Sonst würde das leere else sie verschlucken, wie in der Aufgabe gerade eben.
Genauer, für Neugierige
- Namen von Exceptions enden immer auf
Error. - Deine eigene Exception kann auch von einer spezifischeren fertigen Klasse erben, etwa
RangeError. Dann ist auche instanceof RangeErrorfür sie wahr. Hier ist sie getrennt, deshalb spielt die Reihenfolge der beiden Prüfungen keine Rolle.
Die Idee
Eigene oder fertige?
Checked Exceptions wie in Java gibt es hier nicht. Niemand prüft vor dem Start, ob der Aufrufer fängt. Was eine Funktion wirft, steht in ihrer Dokumentation: ein JSDoc-Kommentar mit @throws.
/** * Hebt den Betrag vom Konto ab. * @throws {RangeError} wenn der Betrag nicht positiv ist * @throws {InsufficientFundsError} wenn das Geld nicht reicht */withdraw(amount) { if (amount <= 0) { throw new RangeError("Der Betrag muss positiv sein"); } if (amount > this.balance) { throw new InsufficientFundsError(amount - this.balance); } this.balance -= amount;}Das ist nur eine Gewohnheit, für Menschen und für den Editor. Ein Programm, das withdraw ohne try aufruft, startet und läuft, bis die Exception wirklich geworfen wird. Die Frage ist eine andere: Brauchst du überhaupt eine eigene Exception?
Eine fertige Exception, wenn die Bedeutung passt: RangeError für einen falschen Wert, TypeError für einen falschen Typ. Eine eigene, wenn der Aufrufer sie von den anderen unterscheiden und auf seine eigene Weise reagieren muss, wenn die Exception Daten tragen muss, wie shortfall, oder wenn es keine fertige mit dieser Bedeutung gibt, etwa für das Abheben von einem geschlossenen Konto.
Probier es aus
Fülle die Lücke
Eine Exception für ein ungültiges Alter. Was gehört in die Lücke, damit die Meldung bei der Basisklasse ankommt?
class InvalidAgeError extends Error {
constructor(message) {
(message);
this.name = "InvalidAgeError";
}
}super(message) ruft den Konstruktor der Basisklasse auf. Die Basisklasse speichert die Meldung, und e.message gibt sie zurück.
Übung
Exceptions in Klassen
Löse die Aufgaben in deinem Editor.
1. Musikgeschäft. Das Geschäft führt Instrumente vor, aber nicht jedes lässt sich vorführen.
- Eine Klasse
MusicalInstrumentmit Name, Marke und Preis und einem Konstruktor. GuitarundPianoerben vom Instrument und haben eine Methodeplay().Drumerbt nur. JavaScript hat keine Interfaces: „kann spielen“ heißt „hat eine Methodeplay“.class NotPlayableError extends Error.MusicShopmit einer MethodedemoInstrument(instrument): Wenn das Instrument spielen kann (typeof instrument.play === "function"), führt sie es vor, sonst wirft sieNotPlayableError.
- Führe im Programm eine Gitarre Fender Stratocaster (1299 €), ein Schlagzeug Pearl Export (849 €) und ein Klavier Yamaha P-45 (499 €) aus einem Array vor. Fange die Exception für jedes Instrument einzeln, damit die Vorführung weitergeht.
Fender Stratocaster, 1299.00 €Die Gitarre spielt einen Akkord.Pearl Export, 849.00 €Pearl Export kann nicht vorgeführt werden.Yamaha P-45, 499.00 €Das Klavier spielt eine Tonleiter.2. Ein Konto mit Regeln. Füge Account eine Methode deposit(amount) hinzu, die bei einem Betrag von 0 oder weniger RangeError wirft. Das Abheben wirft InsufficientFundsError aus der Lektion. Das Konto startet mit 100 €. Lies die Anzahl der Operationen und dann die Operationen selbst, jede in einer eigenen Zeile: „einzahlen 50“ oder „abheben 500“. Gib nach jeder Operation den Kontostand aus oder warum sie abgelehnt wurde.
4einzahlen 50abheben 500abheben -5abheben 120Kontostand: 150 €Es fehlen 350 €. Kontostand: 150 €Der Betrag muss positiv sein. Kontostand: 150 €Kontostand: 30 €3. Ungültiges Alter. Person hat einen Namen und ein Alter. Der Setter set age(value) wirft InvalidAgeError aus der vorigen Karte, wenn das Alter unter 0 oder über 150 liegt. Auch der Konstruktor geht über this.age = age, damit niemand eine Person mit ungültigem Alter erzeugen kann. Erzeuge Maria (19), Peter (−3) und Ivan (200).
Maria, 19 JahrePeter: Ungültiges Alter: -3Ivan: Ungültiges Alter: 200Weitere Aufgaben
- Tischreservierung. Eine Klasse
Restaurantmit einer Anzahl freier Tische und einer Methode zum Reservieren. Wenn keine Tische mehr frei sind, wirf deine eigeneNoTablesError. Wenn die Zahl der Gäste 0 oder kleiner ist, wirf die fertige Exception für ein falsches Argument. - Ein Produkt mit Lagerbestand. Eine Klasse
Productmit einem Lagerbestand und einer Methodesell(qty). Wenn der Bestand nicht reicht, wirfOutOfStockError, die mitträgt, wie viele Stück da sind. Der Shop fängt die Exception und bietet an, die vorhandenen zu verkaufen. - Musikgeschäft mit Grund. Füge
NotPlayableErrorein Feld mit dem Instrument hinzu, das nicht spielen kann, und gib imcatchseinen Preis aus.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Interfaces ohne interface · Typprüfungen · Exceptions · try, catch und finally
- Geht weiter in: Daten in Dateien · Daten, switch und Destrukturierung · Entwurfsmuster · Tests mit node:test
- Aufgaben zum Thema: Fehler und Exceptions