Abschnitt 10 · Lektion 5
Fehler-Middleware
In jeder Route ein try, in jedem catch dieselben vier Zeilen: Das ist die Fassung, die man beim
ersten Mal schreibt, und sie hält bis ungefähr zur achten Route.
Express hat dafür einen eigenen Platz.
Was ohne dich passiert
Erst einmal der Ausgangszustand, denn Express macht schon etwas.
import express from "express";
// Ohne diese Zeile schriebe Express den vollen Stacktrace zusaetzlich ins
// Terminal, mit den Pfaden dieser Maschine. Hier soll nur die Antwort zaehlen.
process.env.NODE_ENV = "test";
const app = express();
app.get("/kaputt", (req, res) => {
throw new Error("Datenbank nicht erreichbar");
});
app.listen(3000); Ein geworfener Fehler stürzt den Server also nicht ab. Express fängt ihn, antwortet mit 500 und schreibt den Stacktrace zusätzlich in die Ausgabe. Für die Entwicklung ist das gar nicht schlecht.
Für den Betrieb ist es untragbar, und du siehst rechts, warum: In der Antwort stehen deine Dateinamen, deine Zeilennummern und die Pfade deiner Pakete. Das ist keine Fehlerseite, das ist ein Bericht über deinen Server. Wer wissen will, womit er gebaut ist, muss nur einen Fehler auslösen.
Der Vollständigkeit halber: Express ist an dieser Stelle nicht naiv. Steht NODE_ENV auf
production, schickt es nur noch Internal Server Error. Aber es bleibt HTML, es bleibt 500 für
alles, und was in dein Protokoll kommt, entscheidest du auch nicht. Für eine Datenschnittstelle ist
das dreimal das Falsche.
Vier Argumente statt drei
import express from "express";
const app = express();
app.get("/kaputt", (req, res) => {
throw new Error("Datenbank nicht erreichbar");
});
app.get("/fehlt", (req, res, next) => {
const fehler = new Error("Buch 99 gibt es nicht");
fehler.status = 404;
next(fehler);
});
// Vier Argumente. Das ist die einzige Kennzeichnung, an der Express eine
// Fehler-Middleware erkennt, und sie gehoert ganz nach unten.
app.use((fehler, req, res, next) => {
const status = fehler.status ?? 500;
res.status(status).json({ fehler: status === 500 ? "Serverfehler" : fehler.message });
});
app.listen(3000); Eine Middleware mit vier Argumenten ist für Express eine Fehler-Middleware. Nicht der Name, nicht die Position, nicht irgendeine Kennzeichnung: allein die Anzahl der Argumente.
Das ist eine Sonderregel, die man nicht erraten kann, und sie hat eine unangenehme Folge. Wenn du
das vierte Argument weglässt, weil du es sowieso nicht brauchst, ist deine Fehler-Middleware
plötzlich eine ganz normale Middleware. Sie wird dann nie aufgerufen, der Fehler landet weiterhin
bei Express, und du suchst den Grund an der falschen Stelle. Schreib next also hin, auch wenn du
es nicht benutzt.
Und sie gehört ganz nach unten, unter alle Routen und alle anderen Middlewares. Das ist dieselbe Regel wie beim Standardfall aus Lektion 9.2: Nur was unter etwas steht, kann von dort etwas entgegennehmen.
Rechts steht /kaputt, und statt des Stacktraces von eben kommt jetzt ein kurzes JSON. Tipp
/fehlt in die Adresszeile für den zweiten Fall.
Schau dir an, was der Handler mit den beiden Fällen macht. Der eine wird zu Serverfehler, der
andere behält seine Meldung. Die Unterscheidung dahinter ist einfach und trägt weit: Was der
Aufrufer selbst ändern kann, darf er erfahren. Was nur du ändern kannst, geht ihn nichts an. Eine
falsche Buch-ID ist sein Fehler, eine unerreichbare Datenbank deiner.
Wie ein Fehler dort ankommt
import express from "express";
const app = express();
// 1. Geworfen in einem gewoehnlichen Handler.
app.get("/geworfen", (req, res) => {
throw new Error("geworfen");
});
// 2. Von Hand per next weitergereicht.
app.get("/weitergereicht", (req, res, next) => {
next(new Error("weitergereicht"));
});
// 3. Geworfen in einem async-Handler. Das faengt Express 5 von selbst ab,
// Express 4 lief hier noch in eine haengende Anfrage.
app.get("/async", async (req, res) => {
await new Promise((r) => setTimeout(r, 5));
throw new Error("aus einem async-Handler");
});
app.use((fehler, req, res, next) => {
res.status(500).json({ weg: fehler.message });
});
app.listen(3000); Rechts steht der erste Weg. /weitergereicht und /async holst du dir über die Adresszeile, und
die Antwort sagt jedes Mal, welcher Weg es war.
Alle drei Wege führen zum selben Handler, und beim dritten lohnt sich ein Blick auf die Versionsnummer.
In Express 4 wurde ein abgelehntes Promise aus einem async-Handler nicht aufgefangen. Die
Anfrage blieb hängen, und in der Ausgabe stand eine Warnung über einen unbehandelten Fehler. Deshalb
findest du in vielen Anleitungen ein Hilfsmittel namens asyncHandler oder ein Paket namens
express-async-errors, und deshalb steht in vielen Projekten in jedem async-Handler ein
try/catch mit next(fehler) darin.
In Express 5 brauchst du beides nicht mehr. Ein abgelehntes Promise landet von selbst beim Fehler-Handler. Falls du auf Code triffst, der das noch von Hand macht: Er ist nicht falsch, nur älter als nötig.
Der Weg über next(fehler) bleibt trotzdem wichtig, und zwar für alles, was kein geworfener
Fehler ist. Ein nicht gefundenes Buch ist keine Ausnahme im Programmablauf, du willst nur denselben
Ausgang benutzen. Dann baust du dir ein Fehlerobjekt, hängst einen Statuscode dran und reichst es
weiter.
Was der Handler tun soll und was nicht
Drei Dinge gehören hinein.
Protokollieren, und zwar vollständig. Nach innen willst du alles: Meldung, Stacktrace, im Idealfall eine Kennung, die zur Anfrage passt. Genau dafür hast du in Lektion 10.2 eine vergeben.
Einen Statuscode wählen. Voreinstellung 500, und wer es besser weiß, hängt status an den
Fehler.
Eine kurze Meldung senden, in derselben Form wie alle anderen Fehler deiner Anwendung. Wie diese Form aussehen sollte, ist Thema von Lektion 11.6.
Und eines gehört nicht hinein: der Stacktrace. Nicht in die Antwort, nie. Das ist derselbe Grundsatz, nach dem auch dieses Repository arbeitet, in dem der Kurs liegt: Interne Details gehen ins Log, nach außen geht eine Meldung, die niemandem verrät, wie es drinnen aussieht.
Zum Mitnehmen
Vier Argumente statt drei, und das ist die einzige Kennzeichnung. Sie steht ganz unten, unter allen Routen, und der Stacktrace bleibt drinnen.
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.