Accueil / Articles

Éliminer le code générique try/catch des routes Express avec asyncHandler

Découvrez comment le middleware d’erreurs, un enveloppe asyncHandler et une classe AppError personnalisée permettent de regrouper les réponses d’erreur d’Express en un seul endroit cohérent, plutôt que dans chaque route.

Éditeur : REACTAPP.TOP5 min de lecture1231 mots

Sources: Stop Repeating try/catch: A Cleaner Error Handling Pattern in Express.js

La plupart des APIs Express commencent par un bloc try/catch dans chaque route asynchrone qui transforme les échecs en une réponse JSON. À mesure que l’API se développe, ce bloc est copié dans chaque gestionnaire. Ici, vous allez regrouper les réponses d’erreur dans un seul middleware, éliminer le try/catch répété au profit d’un petit wrapper, et indiquer les codes d’état à l’aide d’une classe d’erreur personnalisée. Pour les échecs au niveau du processus, consultez notre guide sur la gestion des erreurs de niveau production dans les applications Node.js.

Pourquoi le try/catch par route n’est pas scalable

Une première version consiste généralement à envelopper la logique et à envoyer un code 500 en cas d’échec.

app.get("/users", async (req, res) => {
  try {
    const users = await getUsers();
    res.json(users);
  } catch (err) {
    res.status(500).json({ message: err.message });
  }
});

Rien ne va de mal ici tant que cette structure n’est répétée que dans quelques routes.

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 });
  }
});

Chaque gestionnaire effectue désormais deux tâches : la logique métier et la détermination de l’apparence des erreurs lors de leur transmission. Changer le format signifie modifier chaque route.

Moyen de gestion des erreurs dans Express

Express reconnaît les moyens de gestion des erreurs à leur arité : une fonction ayant quatre paramètres, où l’erreur vient en premier.

(err, req, res, next)

Une version minimale enregistre l’erreur et renvoie un code 500 générique. Insérez-la après vos routes, car Express exécute les moyens de gestion dans l’ordre. Conservez les quatre paramètres même si next n’est pas utilisé, sinon Express le traitera comme un moyen de gestion ordinaire.

app.use((err, req, res, next) => {
  console.error(err);
  res.status(500).json({
    message: "Internal Server Error"
  });
});

Redirection des erreurs avec next(err)

Au lieu de construire la réponse à l’intérieur du bloc catch, la route transmet l’erreur à Express.

app.get("/users", async (req, res, next) => {
  try {
    const users = await getUsers();
    res.json(users);
  } catch (err) {
    next(err);
  }
});

next(err) fait en sorte qu’Express saute par-dessus les moyens de gestion ordinaires pour se rendre directement au gestionnaire d’erreurs :

Request
   ↓
Route
   ↓
Business logic
   ↓
Error?
   ↓
next(err)
   ↓
Centralized error handler
   ↓
HTTP Response

Suppression des try/catch restants

Chaque route contient encore un bloc try/catch. asyncHandler enrobe une fonction de route, l’exécute via Promise.resolve, et envoie toute réjection à next.

const asyncHandler = (fn) => {
  return (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
};

La route se réduit à sa logique réelle :

app.get(
  "/users",
  asyncHandler(async (req, res) => {
    const users = await getUsers();
    res.json(users);
  })
);

Lorsque getUsers() échoue, l’enveloppe appelle :

next(err);

Au sein d’Express 5, les réjections asynchrones sont automatiquement transmises à next, de sorte que l’enveloppe n’a d’importance principalement qu’à Express 4.

Signalement des codes d’état avec AppError

Les erreurs deviennent plus utiles lorsqu’elles incluent un code d’état HTTP :

class AppError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.statusCode = statusCode;
  }
}

Les routes génèrent des erreurs significatives au lieu de formater des réponses :

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);
  })
);

Le gestionnaire lit statusCode, en recourant à 500 en cas d’échec :

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"
  });
});

Un utilisateur manquant génère maintenant une erreur 404 avec :

{
  "success": false,
  "message": "User not found"
}

Prudence : afficher err.message pour chaque erreur peut divulguer des informations internes telles que les messages de la base de données. Ne l'affichez qu’en cas d’instances AppError, sinon utilisez un texte générique. Pour une structure de charge utile standard, consultez notre aperçu des détails du problème RFC 9457.

Une structure de projet complète

Une structure simple :

src/
├── controllers/
│   └── user.controller.js
├── middleware/
│   ├── asyncHandler.js
│   └── errorHandler.js
├── errors/
│   └── AppError.js
├── routes/
│   └── user.routes.js
└── app.js

Le module d’encapsulation

const asyncHandler = (fn) => {
  return (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
};

module.exports = asyncHandler;

Le module de la classe d’erreur

class AppError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.statusCode = statusCode;
  }
}

module.exports = AppError;

Le module de gestion des erreurs

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;

Un contrôleur utilisant les deux

Les contrôleurs récupèrent des données, lancent des AppError si nécessaire, et retournent une structure de succès.

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
};

Le raccordement dans app.js

Le gestionnaire est placé en dernier.

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;

Avantages du pattern

Les contrôleurs cessent de se répéter :

try {
  // ...
} catch (err) {
  res.status(500).json(...);
}

Les clients peuvent s’appuyer sur une seule structure :

{
  "success": false,
  "message": "User not found"
}

La tâche d’un contrôleur devient simple et linéaire :

Get data
↓
Validate
↓
Process
↓
Return result

Le journalage, les IDs de requête et le suivi des erreurs se trouvent dans un seul middleware.

L’ordre est important : enregistrer le gestionnaire en dernier

Il doit suivre tous les routeurs qu’il couvre :

app.use("/users", userRoutes);
app.use("/products", productRoutes);

// Error handler LAST
app.use(errorHandler);

Enregistré plus tôt, il ne voit jamais les erreurs des routes ultérieures.

En résumé

Au lieu de :

Route
 └── try/catch
      └── send error response

on obtient :

Route
 └── business logic
      └── throw error
           ↓
      asyncHandler
           ↓
      centralized error handler
           ↓
      consistent HTTP response

L’objectif réel est de séparer la logique métier de la présentation des erreurs :

  • Le middleware d’erreur nécessite quatre paramètres et doit être enregistré en dernier.
  • Utilisez asyncHandler avec Express 4 ; Express 5 gère lui-même les rejets asynchrones.
  • Lancez une exception AppError avec un statut pour les échecs attendus, et masquez les messages des échecs inattendus.
  • Lectures complémentaires