Abschnitt 16 · Lektion 1
Zwei Arten von Fehlern
In Lektion 3.5 stand ein Satz, der auf später vertröstet hat: manche Fehler kannst du erwarten, manche nicht, und in Abschnitt 16 bekommt diese Unterscheidung einen Namen. Hier ist er.
Betriebsfehler sind Lagen, die im Betrieb vorkommen dürfen. Die Datei ist nicht da. Das Netz ist weg. Die Eingabe ist Unsinn. Die Datenbank ist gerade belegt. Nichts davon ist ein Versagen deines Codes, und für jedes davon soll dein Programm eine Antwort haben.
Programmfehler sind Fehler in deinem Code. Du liest ein Feld, das es nicht gibt. Du rufst eine Funktion mit zwei Argumenten auf, die drei erwartet. Du hast vergessen, einen Wert zu prüfen. Dafür gibt es keine Antwort zur Laufzeit, sondern nur eine Änderung an deinem Code.
Der Unterschied ist keine Wortklauberei
Er entscheidet, was in den catch gehört. Ein catch ist eine Zusage: „für diese Lage habe ich eine
Antwort”. Wer jeden Fehler abfängt, gibt diese Zusage für Lagen ab, die er gar nicht kennt.
{
"monat": "August",
"posten": [
{ "titel": "Kaffee", "betrag": 3.5 },
{ "titel": "Buch", "betrag": 12 }
]
} import { readFileSync } from "node:fs";
// Ein try um die ganze Funktion, und ein catch, der alles gleich behandelt.
function zaehlePosten(pfad) {
try {
const daten = JSON.parse(readFileSync(pfad, "utf8"));
// Tippfehler: das Feld heisst "posten", nicht "positionen".
return daten.positionen.length;
} catch (fehler) {
return 0;
}
}
console.log(`Datei fehlt: ${zaehlePosten("gibtsnicht.json")}`);
console.log(`Datei ist da: ${zaehlePosten("bericht.json")}`); Zweimal dieselbe Zahl, und nur eine davon stimmt. Beim ersten Aufruf fehlt die Datei, und die Null
ist eine ehrliche Antwort. Beim zweiten Aufruf ist die Datei da, sie enthält zwei Posten, und die
Null ist eine Lüge: Das Feld heißt posten, im Code steht positionen, und der catch hat den
TypeError genauso weggeräumt wie vorher das fehlende Verzeichnis.
Das ist der eigentliche Schaden. Nicht dass der Fehler passiert, sondern dass das Programm mit einem falschen Wert weiterläuft und ihn an die nächste Stelle weitergibt. Drei Funktionen später bucht jemand einen Betrag von null, und du suchst dort nach der Ursache, wo sie garantiert nicht ist.
Der try gehört um die Stelle, die scheitern darf
Daraus folgt die praktische Regel: try eng, nicht großzügig. Nur um die Zeile oder die zwei
Zeilen, für die du wirklich einen Plan B hast.
{
"monat": "August",
"posten": [
{ "titel": "Kaffee", "betrag": 3.5 },
{ "titel": "Buch", "betrag": 12 }
]
} import { readFileSync } from "node:fs";
// Derselbe Tippfehler, aber der try liegt nur um die Zeile, die scheitern darf.
function zaehlePosten(pfad) {
let roh;
try {
roh = readFileSync(pfad, "utf8");
} catch (fehler) {
if (fehler.code !== "ENOENT") throw fehler;
return 0;
}
const daten = JSON.parse(roh);
return daten.positionen.length;
}
console.log(`Datei fehlt: ${zaehlePosten("gibtsnicht.json")}`);
console.log(`Datei ist da: ${zaehlePosten("bericht.json")}`); Derselbe Tippfehler, dasselbe Programm, ein anderer Ausgang. Der fehlende Pfad wird abgefangen, und
zwar gezielt über err.code wie in Lektion 3.5. Alles andere fliegt weiter, und das Programm stürzt
mit einem TypeError ab, der die Zeile nennt.
Ein Absturz sieht schlimmer aus als eine Null. Er ist aber besser, und zwar aus einem einzigen Grund: Er passiert an der Stelle, an der der Fehler ist. Der Stacktrace zeigt auf die Zeile, du liest sie, und in zwei Minuten ist es behoben. Die Null zeigt auf nichts.
Nimm das wörtlich: Im Terminal trägt dieses Beispiel drei Fundstellen. Der Ort oben und die
Zeile in zaehlePosten zeigen beide auf Zeile 14, also auf das .length, die dritte auf Zeile 18,
den Aufruf, der dorthin geführt hat. Ein Klick, und der Editor steht darauf. Beim ersten Beispiel
gibt es nichts zu klicken, dort steht eine Null.
Beachte auch die zweite Hälfte des catch: if (fehler.code !== "ENOENT") throw fehler;. Wenn du
schon abfängst, dann wirf zurück, was nicht dazugehört. Ein Rechteproblem oder eine volle Platte
sind auch beim Lesen einer Datei möglich, und für die hat diese Funktion keine Antwort.
Eigene Fehlerklassen
Bis hierhin hast du auf err.code geprüft, und das funktioniert, solange Node den Fehler wirft. Für
deine eigenen Fehler brauchst du dasselbe: etwas, worauf weiter oben jemand reagieren kann, ohne
Meldungstexte zu vergleichen.
// Eine eigene Fehlerklasse: ein Name, ein Code und der ursprüngliche Fehler.
class KonfigFehler extends Error {
constructor(meldung, ursache) {
super(meldung, { cause: ursache });
this.name = "KonfigFehler";
this.code = "konfig_kaputt";
}
}
function ladeKonfig(roh) {
try {
return JSON.parse(roh);
} catch (fehler) {
throw new KonfigFehler("Die Konfiguration ist kein gültiges JSON", fehler);
}
}
try {
ladeKonfig("{ waehrung: EUR }");
} catch (fehler) {
console.log(`Name: ${fehler.name}`);
console.log(`Code: ${fehler.code}`);
console.log(`Meldung: ${fehler.message}`);
console.log(`Ursache: ${fehler.cause.name}`);
console.log(`Ist ein KonfigFehler: ${fehler instanceof KonfigFehler}`);
console.log(`Ist ein Error: ${fehler instanceof Error}`);
} class KonfigFehler extends Error ist alles, was dazu nötig ist. Drei Dinge kommen dabei mit:
- Ein
name, damit die Ausgabe im Protokoll den Fehler benennt statt nur „Error”. - Ein
code, damit eine Prüfung weiter oben stabil ist. Meldungstexte formuliert man um, übersetzt sie oder ergänzt sie, und jedes Mal geht eine Prüfung auf den Text kaputt, ohne dass es jemand merkt. Genau dieselbe Regel wie beierr.codein 3.5, nur jetzt für deine Fehler. instanceoffunktioniert nebenbei mit, in beide Richtungen: einKonfigFehlerist einKonfigFehlerund einError. Eincatchweiter oben kann also grob oder fein reagieren.
Was cause dir spart
Der zweite Parameter von super(meldung, { cause: ursache }) ist die eingebaute Antwort auf ein
altes Problem: Du fängst einen Fehler, wirfst einen aussagekräftigeren, und der ursprüngliche ist
weg.
Mit cause hängt er dran. Oben steht dein Satz („die Konfiguration ist kein gültiges JSON”), und
darunter hängt der SyntaxError, der es wirklich gemerkt hat, mit seinem eigenen Stacktrace. Node
zeigt die Kette beim Ausgeben von selbst an, und fehler.cause kommt im Code an jede Ebene heran.
Verwechsle das nicht mit dem Weiterreichen: throw fehler; gibt denselben Fehler weiter,
throw new EigenerFehler("...", fehler); legt eine Schicht Bedeutung darüber und behält die alte.
Die Faustregel
Frag dich vor jedem catch eine einzige Sache: Habe ich für diese Lage eine Antwort?
Wenn ja, dann fang genau sie ab, und zwar an code oder instanceof erkannt, nicht an einem Text.
Wenn nein, dann lass sie durch. Ein Programm, das an der richtigen Stelle abstürzt, ist mehr wert als
eines, das überall weiterläuft und nirgends stimmt.
Zum Mitnehmen
Ein Betriebsfehler ist eine Lage, auf die dein Programm eine Antwort haben soll. Ein Programmfehler ist ein Fehler in deinem Code. Der erste gehört behandelt, der zweite behoben, und das Schlimmste ist, den zweiten wie den ersten zu behandeln.
Jetzt du
Basis Konto, kostenlosZu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.
Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.
In diesem Kurs läuft dein Code auf einem Server. Dafür hat das Basis Konto 1 Stunde im Monat, mehr Zeit gibt es mit dem Premium Konto.
Was in dieser Lektion steckt
-
Artikel mit 3 Beispielen zum Ausprobieren
Steht hier, ohne Konto lesbar.
-
Aufgabe, dein Code läuft auf einem Server
Öffnet sich mit dem Basis Konto.