Zum Inhalt
wannacode

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} €`);
Ausgabe
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} €`);
Ausgabe
Abgelehnt: Nicht genug Geld
Kontostand: 100 €
  • new erzeugt das Exception-Objekt wie jedes andere Objekt. Die Meldung kommt in seinen Konstruktor, und e.message gibt sie zurück.
  • RangeError ist 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 du Error oder eine eigene Exception.
  • throw nimmt in JavaScript jeden Wert an, sogar einen String: throw "Nicht genug Geld" funktioniert. Aber dann hat catch kein e.message und kein e.stack (beide sind undefined), und es gibt keinen Stacktrace. Wirf deshalb nur Error oder 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
  1. Das Programm ruft payRent für ein Konto mit 500 € auf.
  2. payRent ruft withdraw(800) auf: ein neuer Frame oben drauf.
  3. 800 > 500: Die Methode wirft eine Exception.
  4. Der Kontostand ändert sich nicht: Der Frame wird verworfen, ohne seine Zeilen zu Ende auszuführen.
  5. payRent hat kein catch: „Die Miete ist bezahlt.“ wird nicht ausgegeben, und der Frame wird verworfen.
  6. 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));
Fehler
/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.2

Node 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} €`);
Antwort

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.

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; im catch. Der Stacktrace bleibt der ursprüngliche: Er wird aufgezeichnet, wenn der Error erzeugt 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}`);
}
Ausgabe
Log: Die Miete ist nicht durchgegangen
Abgelehnt: 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 in e.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}`);
}
Ausgabe
Die Miete ist nicht bezahlt, weil: Nicht genug Geld

Die 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;
}
}
Ausgabe
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 auch e instanceof RangeError fü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";
  }
}

Ü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 MusicalInstrument mit Name, Marke und Preis und einem Konstruktor.
  • Guitar und Piano erben vom Instrument und haben eine Methode play(). Drum erbt nur. JavaScript hat keine Interfaces: „kann spielen“ heißt „hat eine Methode play“.
  • class NotPlayableError extends Error.
  • MusicShop mit einer Methode demoInstrument(instrument): Wenn das Instrument spielen kann (typeof instrument.play === "function"), führt sie es vor, sonst wirft sie NotPlayableError.
  • 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.
Ausgabe
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.

Eingabe
4
einzahlen 50
abheben 500
abheben -5
abheben 120
Ausgabe
Kontostand: 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).

Ausgabe
Maria, 19 Jahre
Peter: Ungültiges Alter: -3
Ivan: Ungültiges Alter: 200

Weitere Aufgaben

  • Tischreservierung. Eine Klasse Restaurant mit einer Anzahl freier Tische und einer Methode zum Reservieren. Wenn keine Tische mehr frei sind, wirf deine eigene NoTablesError. 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 Product mit einem Lagerbestand und einer Methode sell(qty). Wenn der Bestand nicht reicht, wirf OutOfStockError, 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 NotPlayableError ein Feld mit dem Instrument hinzu, das nicht spielen kann, und gib im catch seinen Preis aus.

Test

Teste dich

1. Was macht throw new RangeError("...")?
2. Das Programm ruft a auf, a ruft b auf, b wirft eine Exception. Nur der Code auf oberster Ebene hat ein catch. Was passiert mit den Zeilen in a nach dem Aufruf von b?
3. Wo fängst du die Exception am besten?
4. Warum setzt der Konstruktor von InsufficientFundsError this.name = "InsufficientFundsError"?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·