backend / nodejs
Gestion d'erreurs centralisée
Explication
Ce que vous allez apprendre
- Distinguer une erreur métier "attendue" d'un bug véritable avec
isOperational - Créer une hiérarchie de classes d'erreurs typées (
NotFoundError,ValidationError...) - Centraliser le formatage de toutes les réponses d'erreur dans un middleware unique
- Comprendre pourquoi
uncaughtException/unhandledRejectionsont un dernier filet, pas une stratégie - Éviter de dupliquer la logique de formatage d'erreur dans chaque contrôleur
Dans quel contexte ?
Sur une API en croissance, certaines routes renvoient { "error": "..." }, d'autres { "message": "..." }, et une route oublie même de renvoyer du JSON en cas d'erreur, cassant le client qui tente de parser la réponse. Cette incohérence vient du fait que chaque développeur a géré les erreurs à sa façon, route par route. Cette leçon montre comment centraliser ce traitement pour que toute l'API réponde aux erreurs de façon prévisible, avec les mêmes clés partout.
Le problème, d'abord
Sans stratégie claire, chaque route finit par gérer ses erreurs à sa façon. Certaines renvoient un texte brut, d'autres un JSON avec des clés différentes, d'autres encore laissent fuiter la stack trace complète au client.
Le résultat est une API incohérente, difficile à consommer, et potentiellement dangereuse. Des informations internes peuvent se retrouver exposées à n'importe quel client.
L'idée centrale pour corriger ça est de distinguer deux types d'erreurs très différents. Une erreur "métier", comme un utilisateur introuvable ou une validation qui échoue, est un événement NORMAL et PRÉVU du fonctionnement de l'application.
Elle mérite une réponse HTTP claire (404, 400), pas un traitement comme une catastrophe. Un vrai bug, comme une exception inattendue ou une connexion base de données perdue, est différent : il doit être loggé en détail pour investigation, mais SANS exposer ses détails techniques au client.
| Type d'erreur | Exemple | isOperational | Réponse au client |
|---|---|---|---|
| Métier (attendue) | Utilisateur introuvable, validation échouée | true | Status précis (404, 400) + message clair |
| Bug (inattendu) | Exception non prévue, connexion DB perdue | false ou absent | 500 générique, détails loggés côté serveur |
Prérequis
Cette leçon suppose que tu es à l'aise avec le middleware d'erreur central vu à la leçon précédente : ici, on structure les erreurs qu'il reçoit plutôt que le middleware lui-même.
Cette distinction se code très concrètement avec une propriété : isOperational. Elle marque une erreur comme "attendue" plutôt que comme un bug véritable.
Une fois cette distinction posée, comment l'organiser proprement dans le code ? Plutôt que des if dispersés partout, on crée une hiérarchie de classes d'erreurs typées (NotFoundError, ValidationError, UnauthorizedError) héritant toutes d'une classe AppError commune.
Ça permet au middleware d'erreur central de savoir immédiatement comment réagir. Un simple err instanceof AppError suffit, sans deviner ni dupliquer cette logique dans chaque route.
Grâce à ce système, chaque contrôleur peut rester simple. Il fait throw new NotFoundError(...) sans se soucier du formatage final de la réponse — cette responsabilité est entièrement déléguée au middleware d'erreur central vu dans la leçon précédente.
Il reste un dernier filet de sécurité à connaître, pour le pire des cas. uncaughtException et unhandledRejection ne sont PAS un mécanisme de gestion d'erreurs normal, mais des événements signalant qu'une erreur est passée à travers toutes les protections.
Un processus dans cet état est considéré comme incertain. La bonne pratique est de logger, fermer proprement les connexions, puis quitter le processus plutôt que de continuer à tourner dans un état potentiellement corrompu.
Piège dangereux
Ignorer unhandledRejection en se disant "ça n'arrivera jamais" laisse le processus continuer à tourner dans un état incertain après une vraie erreur non gérée. Toujours logger l'erreur, fermer proprement les connexions ouvertes, puis quitter le processus plutôt que de le laisser survivre dans un état potentiellement corrompu.
Commandes & code
Gestion d'erreurs centralisée
// errors/AppError.js — hiérarchie d'erreurs métier typées
export class AppError extends Error {
constructor(message, status = 500, code = "INTERNAL_ERROR") {
super(message);
this.name = this.constructor.name;
this.status = status;
this.code = code;
this.isOperational = true; // distingue erreur "attendue" vs bug
Error.captureStackTrace(this, this.constructor);
}
}
export class NotFoundError extends AppError {
constructor(resource = "Ressource") {
super(`${resource} introuvable`, 404, "NOT_FOUND");
}
}
export class ValidationError extends AppError {
constructor(details) {
super("Données invalides", 400, "VALIDATION_ERROR");
this.details = details;
}
}
export class UnauthorizedError extends AppError {
constructor(message = "Authentification requise") {
super(message, 401, "UNAUTHORIZED");
}
}// Utilisation dans un contrôleur — pas de try/catch dispersé partout
import { NotFoundError, ValidationError } from "../errors/AppError.js";
export async function getOrder(req, res, next) {
try {
const order = await db.orders.findById(req.params.id);
if (!order) throw new NotFoundError("Commande");
res.json(order);
} catch (err) {
next(err); // toujours transmettre au middleware d'erreur central
}
}
export async function createOrder(req, res, next) {
try {
const { items } = req.body;
if (!Array.isArray(items) || items.length === 0) {
throw new ValidationError({ items: "Doit contenir au moins un article" });
}
const order = await db.orders.create({ items });
res.status(201).json(order);
} catch (err) {
next(err);
}
}// middlewares/errorHandler.js — point d'entrée UNIQUE pour toutes les erreurs
import { AppError } from "../errors/AppError.js";
export function errorHandler(err, req, res, next) {
// Erreur métier connue : réponse propre et prévisible
if (err instanceof AppError) {
const body = { error: err.message, code: err.code };
if (err.details) body.details = err.details;
return res.status(err.status).json(body);
}
// Erreur de validation d'une lib tierce (ex. Zod)
if (err.name === "ZodError") {
return res.status(400).json({ error: "Validation échouée", details: err.flatten() });
}
// Erreur inattendue : log complet, réponse générique (pas de fuite d'infos internes)
console.error("Erreur non gérée:", err);
res.status(500).json({ error: "Erreur interne du serveur" });
}// Capturer les erreurs globales non gérées — dernier filet de sécurité
process.on("uncaughtException", (err) => {
console.error("Exception non capturée:", err);
// fermeture propre puis arrêt : un uncaughtException laisse le process dans un état incertain
gracefulShutdown(() => process.exit(1));
});
process.on("unhandledRejection", (reason) => {
console.error("Promise rejetée non gérée:", reason);
gracefulShutdown(() => process.exit(1));
});
function gracefulShutdown(callback) {
server.close(() => {
db.close();
callback();
});
setTimeout(() => callback(), 10_000); // force l'arrêt après 10s si le close traîne
}Résumé
- Une hiérarchie de classes d'erreurs (
AppErroret sous-classes) rend les erreurs métier explicites et typées. - Un middleware d'erreur central évite de dupliquer la logique de formatage de réponse d'erreur.
isOperationaldistingue une erreur "attendue" (404, validation) d'un bug véritable à investiguer.uncaughtException/unhandledRejectionsont un filet de sécurité, pas un mécanisme de gestion d'erreurs normal.
Exercices pratiques
Mission : unifier des réponses d'erreur incohérentes avant un audit client
Objectif : Remplacer une gestion d'erreurs incohérente par une hiérarchie AppError typée et un middleware d'erreur central unique, puis sécuriser l'arrêt du processus.
Contexte
Un client externe qui consomme l'API se plaint : GET /orders/999 (commande inexistante) renvoie {"message": "not found"}, tandis que POST /orders avec un body invalide renvoie un texte brut "Bad request: items required" sans JSON du tout, et une troisième route laisse fuiter err.stack complet dans la réponse 500 en cas de bug. Aucune route ne suit le même format, ce qui casse le SDK client généré automatiquement à partir d'un format attendu unique.