Startseite / Artikel

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.

Verlag: REACTAPP.TOP5 Min. Lesezeit1231 Wörter

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.
  • Verwenden Sie asyncHandler in Express 4; Express 5 leitet asynchrone Ablehnungen selbst weiter.
  • Werfen Sie bei erwarteten Fehlern einen AppError mit dem entsprechenden Status aus und verbergen Sie Nachrichten bei unerwarteten Fehlern.