Entfernung des Standardcodes try/catch aus Express-Routen mit asyncHandler
Erfahren Sie, wie Fehler-Middleware, ein asyncHandler-Wrapper sowie eine benutzerdefinierte AppError-Klasse die Fehlerantworten von Express aus allen Routen in einen einheitlichen Ort verlagern.
Quellen: Stop Repeating try/catch: A Cleaner Error Handling Pattern in Express.js
Die meisten Express-APIs beginnen mit einem try/catch-Block in jeder asynchronen Route, der Fehler in eine JSON-Antwort umwandelt. Wenn die API wächst, wird dieser Block in jeden Handler kopiert. Hier werden die Fehlerantworten in ein einziges Middleware-Modul verlagert, das wiederholte try/catch-Konstrukt durch eine kleine Wrapper-Funktion ersetzt und Statuscodes mithilfe einer benutzerdefinierten Fehlerklasse signalisiert. Für fehlerhafte Abläufe auf Prozessebene siehe unseren Leitfaden zu produktionsreifem Fehlerbehandlungsansatz in Node.js.
Warum try/catch pro Route nicht skalierbar ist
In der ersten Version wird in der Regel die Logik abgepackt und bei einem Fehler eine 500-Antwort gesendet.
app.get("/users", async (req, res) => {
try {
const users = await getUsers();
res.json(users);
} catch (err) {
res.status(500).json({ message: err.message });
}
});
Es gibt zunächst kein Problem, solange sich diese Struktur nur in wenigen Routen wiederholt.
app.get("/users", async (req, res) => {
try {
// ...
} catch (err) {
res.status(500).json({ message: err.message });
}
});
app.get("/products", async (req, res) => {
try {
// ...
} catch (err) {
res.status(500).json({ message: err.message });
}
});
app.post("/orders", async (req, res) => {
try {
// ...
} catch (err) {
res.status(500).json({ message: err.message });
}
});
Jeder Handler erledigt nun zwei Aufgaben: die Geschäftslogik sowie die Festlegung, wie Fehler übertragen werden. Eine Änderung des Formats bedeutet, dass jede Route bearbeitet werden muss.
Fehlerbehandlungs-Middleware in Express
Express erkennt Fehler-Middleware an ihrer Arity: eine Funktion mit vier Parametern, wobei der Fehler an erster Stelle steht.
(err, req, res, next)
Eine minimale Version protokolliert den Fehler und gibt einen generischen 500-Fehler zurück. Registrieren Sie sie nach Ihren Routen, da Express die Middleware in der Reihenfolge ausführt. Behalten Sie alle vier Parameter bei, auch wenn next nicht verwendet wird – sonst behandelt Express es als gewöhnliche Middleware.
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({
message: "Internal Server Error"
});
});
Fehler weiterleiten mit next(err)
Anstatt die Antwort innerhalb von catch zu erstellen, übergibt die Route den Fehler an Express.
app.get("/users", async (req, res, next) => {
try {
const users = await getUsers();
res.json(users);
} catch (err) {
next(err);
}
});
next(err) lässt Express die reguläre Middleware überspringen und direkt zum Fehlerhandler wechseln:
Request
↓
Route
↓
Business logic
↓
Error?
↓
next(err)
↓
Centralized error handler
↓
HTTP Response
Entfernen des verbleibenden try/catch
Jede Route enthält weiterhin ein try/catch. asyncHandler umhüllt eine Route-Funktion, führt sie über Promise.resolve aus und leitet alle Ablehnungen an next weiter.
const asyncHandler = (fn) => {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
};
Die Route reduziert sich auf ihre eigentliche Logik:
app.get(
"/users",
asyncHandler(async (req, res) => {
const users = await getUsers();
res.json(users);
})
);
Wenn getUsers() abgelehnt wird, ruft der Wrapper auf:
next(err);
In Express 5 werden asynchrone Ablehnungen automatisch an next weitergeleitet, weshalb der Wrapper hauptsächlich in Express 4 von Bedeutung ist.
Signalisierung von Statuscodes mit AppError
Fehler sind nützlicher, wenn sie einen HTTP-Statuscode enthalten:
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
}
}
Die Routen werfen sinnvolle Fehler aus anstelle, Antworten zu formatieren:
app.get(
"/users/:id",
asyncHandler(async (req, res) => {
const user = await getUser(req.params.id);
if (!user) {
throw new AppError("User not found", 404);
}
res.json(user);
})
);
Der Handler liest statusCode und fällt bei Bedarf auf 500 zurück:
app.use((err, req, res, next) => {
console.error(err);
const statusCode = err.statusCode || 500;
res.status(statusCode).json({
success: false,
message: err.message || "Internal Server Error"
});
});
Ein fehlender Benutzer führt nun zu einer 404-Fehlermeldung mit:
{
"success": false,
"message": "User not found"
}
Achtung: Das Ausgeben von err.message für jeden Fehler kann interne Informationen wie Datenbanknachrichten preisgeben. Geben Sie diesen Wert nur für AppError-Instanzen aus und verwenden Sie ansonsten einen allgemeinen Text. Für eine standardmäßige Payload-Struktur siehe unsere Übersicht zu den Details des RFC 9457-Problems.
Eine vollständige Projektstruktur
Eine einfache Struktur:
src/
├── controllers/
│ └── user.controller.js
├── middleware/
│ ├── asyncHandler.js
│ └── errorHandler.js
├── errors/
│ └── AppError.js
├── routes/
│ └── user.routes.js
└── app.js
Das Wrapper-Modul
const asyncHandler = (fn) => {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
};
module.exports = asyncHandler;
Das Fehlerklassen-Modul
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
}
}
module.exports = AppError;
Das Fehlerbehandlungsmodul
const errorHandler = (err, req, res, next) => {
console.error(err);
res.status(err.statusCode || 500).json({
success: false,
message: err.message || "Internal Server Error"
});
};
module.exports = errorHandler;
Ein Controller, der beides verwendet
Controller holen Daten ab, werfen bei Bedarf AppError aus und geben schließlich eine Erfolgsstruktur zurück.
const asyncHandler = require("../middleware/asyncHandler");
const AppError = require("../errors/AppError");
const getUsers = asyncHandler(async (req, res) => {
const users = await userService.getUsers();
res.json({
success: true,
data: users
});
});
const getUser = asyncHandler(async (req, res) => {
const user = await userService.getUser(req.params.id);
if (!user) {
throw new AppError("User not found", 404);
}
res.json({
success: true,
data: user
});
});
module.exports = {
getUsers,
getUser
};
Einbinden in app.js
Die Fehlerbehandlung wird zuletzt eingefügt.
const express = require("express");
const userRoutes = require("./routes/user.routes");
const errorHandler = require("./middleware/errorHandler");
const app = express();
app.use(express.json());
app.use("/users", userRoutes);
// Must be after routes
app.use(errorHandler);
module.exports = app;
Was das Muster Ihnen bringt
Controller müssen nicht mehr wiederholt werden:
try {
// ...
} catch (err) {
res.status(500).json(...);
}
Einheitliches Fehlerformat
Kunden können sich auf eine einheitliche Struktur verlassen:
{
"success": false,
"message": "User not found"
}
Fokussierte Controller
Die Aufgabe eines Controllers wird einfacher:
Get data
↓
Validate
↓
Process
↓
Return result
Ein einziger Ort für Überwachbarkeit
Logging, Anfragenummern und Fehlerverfolgung befinden sich im selben Middleware-Modul.
Reihenfolge ist wichtig: Registrieren Sie den Handler zuletzt
Er muss allen von ihm abgedeckten Routern folgen:
app.use("/users", userRoutes);
app.use("/products", productRoutes);
// Error handler LAST
app.use(errorHandler);
Wird früher registriert, sieht er keine Fehler aus späteren Routen.
Zusammenfassung
Anstelle von:
Route
└── try/catch
└── send error response
wird:
Route
└── business logic
└── throw error
↓
asyncHandler
↓
centralized error handler
↓
consistent HTTP response
Das eigentliche Ziel ist es, die Geschäftslogik von der Fehleranzeige zu trennen:
- Das Fehler-Middleware-Modul benötigt vier Parameter und muss zuletzt registriert werden.
asyncHandler in Express 4; Express 5 leitet asynchrone Ablehnungen selbst weiter.AppError mit dem entsprechenden Status aus und verbergen Sie Nachrichten bei unerwarteten Fehlern.