Warum Fehler unvermeidlich sind
Jedes Programm gerät irgendwann in eine Situation, mit der es nicht gerechnet hat: Ein Server gibt einen 500-Fehler zurück, ein Nutzer gibt Buchstaben in ein numerisches Feld ein, eine Datei fehlt oder eine Drittanbieter-API ändert ihr Format. Bei der Fehlerbehandlung geht es nicht darum, jeden Ausfall zu verhindern – sondern darum, zu entscheiden, was passiert, wenn einer auftritt.
Gut behandelte Fehler halten eine Anwendung am Laufen, geben dem Nutzer hilfreiche Informationen und liefern Entwicklern die Details, die sie benötigen. Schlecht behandelte Fehler führen zu leeren Bildschirmen, lautlosem Datenverlust und Bug-Reports, die unmöglich zu reproduzieren sind.
Das Error-Objekt
JavaScript verfügt über einen integrierten Error-Typ, und jeder geworfene Fehler ist in der Regel eine Instanz davon.
// error.js
const error = new Error("Something broke");
error.message; // "Something broke"
error.name; // "Error"
error.stack; // the call stack at the point of creation
error.cause; // an optional underlying error
Es gibt mehrere integrierte Unterklassen für gängige Kategorien: TypeError, ReferenceError, RangeError, SyntaxError, URIError und EvalError. Diese unterscheiden sich hauptsächlich im Namen und in den Situationen, die sie auslösen, was sie nützlich für das Debugging und für die Verzweigung basierend auf der Art des Fehlers macht.
Fehler auslösen
Verwenden Sie throw, um zu signalisieren, dass eine Regel verletzt wurde. Werfen Sie ein Error-Objekt, niemals einen String oder eine Zahl – nur Error-Instanzen enthalten eine Nachricht, einen Namen und einen Stack Trace.
// validate.js
function divide(a, b) {
if (b === 0) {
throw new Error("Cannot divide by zero");
}
return a / b;
}
Das frühzeitige Auslösen von Fehlern – genau an dem Punkt, an dem eine Annahme nicht mehr zutrifft – verhindert, dass sich ein ungültiger Zustand tiefer in Ihrem Programm ausbreitet. Dass Fehler in der Entwicklung lautstark gemeldet werden, ist ein Feature und kein Ärgernis.
try, catch und finally
try führt riskanten Code aus. Wenn ein Fehler geworfen wird, springt die Steuerung zu catch. finally wird in jedem Fall ausgeführt, was es zum idealen Ort für Cleanup-Aufgaben macht.
// try.js
try {
const data = JSON.parse(input);
save(data);
} catch (error) {
console.error("Invalid data:", error.message);
} finally {
setLoading(false);
}
Der Parameter catch ist der geworfene Wert. Modernes JavaScript unterstützt zudem optional catch binding, wenn dieser Wert nicht benötigt wird: catch { ... }. Innerhalb eines catch-Blocks können Sie den Fehler untersuchen, ihn loggen, den Zustand wiederherstellen oder den Fehler erneut werfen, falls Sie ihn nicht behandeln können.
// rethrow.js
try {
await loadProfile();
} catch (error) {
log(error);
throw error; // let a higher layer decide
}
Benutzerdefinierte Error-Klassen
Generische Fehler verlieren an Bedeutung, sobald Ihre App wächst. Benutzerdefinierte Klassen ermöglichen es Ihnen, Kontext hinzuzufügen und basierend auf der Art des Fehlers unterschiedlich zu reagieren.
// http-error.js
class HttpError extends Error {
constructor(status, message) {
super(message ?? `HTTP ${status}`);
this.name = "HttpError";
this.status = status;
}
}
class ValidationError extends Error {
constructor(field, message) {
super(message);
this.name = "ValidationError";
this.field = field;
}
}
Nun können Aufrufer präzise reagieren:
// handle.js
try {
await save(form);
} catch (error) {
if (error instanceof ValidationError) {
showFieldError(error.field, error.message);
} else if (error instanceof HttpError && error.status === 401) {
redirectToLogin();
} else {
showGenericError();
}
}
Das ist der Unterschied zwischen „etwas ist schiefgelaufen“ und „das E-Mail-Feld ist bereits belegt“.
Fehler mit cause umschließen
Manchmal möchte man Kontext hinzufügen, ohne die ursprüngliche Fehlerursache zu verlieren. Die Option cause bewahrt die Fehlerkette.
// cause.js
try {
await db.query(sql);
} catch (error) {
throw new Error("Failed to load orders", { cause: error });
}
Der äußere Fehler enthält den benutzerfreundlichen Kontext, während error.cause die ursprünglichen Details für Logs und das Debugging beibehält.
Fehler in asynchronem Code
Asynchroner Code hat zwei zusätzliche Fehlerpfade. Bei async/await funktioniert try/catch exakt so wie bei synchronem Code.
// async.js
async function load() {
try {
const res = await fetch("/api/data");
if (!res.ok) throw new HttpError(res.status);
return await res.json();
} catch (error) {
console.error("Load failed:", error);
throw error;
}
}
Verwendest du kein await, hänge einen Handler an das Promise an:
// promise.js
fetch("/api/data")
.then((res) => res.json())
.catch((error) => console.error(error));
Ein abgelehntes Promise ohne Handler wird zu einer unhandled rejection. In Browsern wird eine Warnung protokolliert; in Node.js kann dies den Prozess beenden. Behandle Rejections immer, und wenn es nur dazu dient, sie zu loggen und erneut zu werfen.
Globale Sicherheitsnetze
Selbst bei sorgfältigem Code wird etwas durchrutschen. Globale Handler fangen das auf, was Sie übersehen haben, damit die App den Fehler melden kann, anstatt lautlos abzustürzen.
// global.js
window.addEventListener("error", (event) => {
reportToServer(event.error);
});
window.addEventListener("unhandledrejection", (event) => {
reportToServer(event.reason);
});
In Node.js sind die entsprechenden Mechanismen process.on("uncaughtException") und process.on("unhandledRejection"). Betrachten Sie diese als letzte Instanz für das Logging und ein Graceful Shutdown, nicht als Ersatz für die Fehlerbehandlung an der Stelle, an der sie auftreten.
Validieren, bevor geworfen wird
Nicht jedes Problem ist eine Ausnahme. Erwartbare Bedingungen – leere Eingaben, ein fehlendes optionales Feld, eine Suche ohne Ergebnisse – lassen sich besser durch Validierung als durch Exceptions handhaben.
// guards.js
function formatName(user) {
if (!user?.name) return "Anonymous";
return user.name.trim();
}
Reservieren Sie Exceptions für wirklich unerwartete oder nicht behebbare Situationen. Wenn sie für den gewöhnlichen Kontrollfluss verwendet werden, wird der Code langsamer und schwerer nachvollziehbar.
Kommunikation mit Nutzern
Technische Fehlermeldungen sind für Entwickler gedacht. Nutzer müssen hingegen wissen, was passiert ist und was sie als Nächstes tun können.
- Halten Sie Nachrichten kurz und menschlich: „Wir konnten Ihre Änderungen nicht speichern. Bitte prüfen Sie Ihre Verbindung und versuchen Sie es erneut.“
- Vermeiden Sie Jargon, Stack Traces und rohe Statuscodes in der Benutzeroberfläche.
- Bieten Sie einen nächsten Schritt an: Erneut versuchen, zurückgehen, den Support kontaktieren oder offline fortfahren.
- Loggen Sie die vollständigen Details — Nachricht, Stack, Kontext —, damit der Support den Fehler untersuchen kann.
Best Practices
- Werfen Sie
Error-Objekte, niemals Strings. - Nutzen Sie
catchnur dort, wo Sie den Fehler beheben oder nützlichen Kontext hinzufügen können. - Verwenden Sie benutzerdefinierte Error-Klassen für domänenspezifische Fehler.
- Verpacken Sie Fehler mit
cause, anstatt das Original zu verwerfen. - Behandeln Sie Promise-Rejections immer.
- Implementieren Sie globale Handler als letztes Sicherheitsnetz und loggen Sie diese an einen echten Service.
- Validieren Sie erwartete Bedingungen, anstatt Fehler zu werfen.
- Trennen Sie Benutzermeldungen von Diagnosen für Entwickler.
- Schreiben Sie Tests für Ihre Fehlerpfade, nicht nur für den Happy Path.
Häufige Fehler
- Fehler durch einen leeren
catch-Block einfach “verschlucken”. - Strings werfen und dadurch den Stack Trace verlieren.
- Einen Fehler abfangen und einen Standardwert zurückgeben, der einen echten Bug verschleiert.
- Vergessen, rejected Promises zu behandeln.
- Rohe Fehlermeldungen an Benutzer anzeigen.
- Exceptions für den gewöhnlichen Control Flow verwenden.
- Davon ausgehen, dass
try/catchum einen Promise-Executor herum asynchrone Fehler abfängt — tut es nicht.
Wie geht es weiter?
Fehler treten dort auf, wo asynchroner Code und die Fetch API auf die Realität treffen. Kombinieren Sie die hier vorgestellten Muster mit dem DOM, um benutzerfreundliche Zustände darzustellen, und kehren Sie zu den JavaScript-Grundlagen zurück, wann immer Sie ein TypeError daran erinnert, dass undefined keine Funktion ist.