Gestion des erreurs en Node.js : les bonnes pratiques
Mark Toledo
1 avril 2026

La gestion des erreurs est un sujet que j'ai longtemps sous-estimé. En début de carrière, mes APIs Node.js avaient des try/catch placés au hasard, des console.error sans structure, et une tendance à laisser des erreurs non gérées faire planter le processus silencieusement. J'ai appris à mes dépens qu'une mauvaise gestion des erreurs est souvent la différence entre un service fiable et un service qu'on redémarre toutes les heures à 3h du matin.
Ce guide couvre les patterns que j'utilise en production.
Deux types d'erreurs, deux réponses différentes
La distinction la plus importante en Node.js : erreurs opérationnelles vs erreurs de programmeur.
Erreurs opérationnelles : attendues, récupérables. L'utilisateur a envoyé une mauvaise requête, la base de données est temporairement inaccessible, un fichier n'existe pas. Ces erreurs font partie du flux normal de l'application.
Erreurs de programmeur : bugs dans le code. Accès à une propriété d'un objet null, argument invalide passé à une fonction interne. Ces erreurs ne devraient pas arriver en production — si elles arrivent, c'est qu'il faut corriger le code.
// Erreur opérationnelle — à gérer gracieusement
const utilisateur = await db.trouverParId(id);
if (!utilisateur) {
throw new ErreurNonTrouve(`Utilisateur ${id} introuvable`);
}
// Erreur de programmeur — indique un bug
function calculerTotal(articles) {
if (!Array.isArray(articles)) {
// Ce cas ne devrait jamais arriver si le code est correct
throw new TypeError('articles doit être un tableau');
}
return articles.reduce((sum, a) => sum + a.prix, 0);
}
Classes d'erreurs personnalisées
Créer des classes d'erreurs spécialisées rend le code plus expressif et permet de distinguer les types d'erreurs dans les gestionnaires.
// errors/index.js
class ErreurApp extends Error {
constructor(message, code, statusCode = 500) {
super(message);
this.name = this.constructor.name;
this.code = code;
this.statusCode = statusCode;
this.isOperational = true; // Marquer comme erreur opérationnelle
Error.captureStackTrace(this, this.constructor);
}
}
class ErreurValidation extends ErreurApp {
constructor(message, champs = {}) {
super(message, 'VALIDATION_ERROR', 400);
this.champs = champs;
}
}
class ErreurNonTrouve extends ErreurApp {
constructor(ressource) {
super(`${ressource} introuvable`, 'NOT_FOUND', 404);
this.ressource = ressource;
}
}
class ErreurNonAutorise extends ErreurApp {
constructor(message = 'Non autorisé') {
super(message, 'UNAUTHORIZED', 401);
}
}
class ErreurConflict extends ErreurApp {
constructor(message) {
super(message, 'CONFLICT', 409);
}
}
export { ErreurApp, ErreurValidation, ErreurNonTrouve, ErreurNonAutorise, ErreurConflict };
Utilisation dans les services :
import { ErreurNonTrouve, ErreurConflict } from '../errors/index.js';
async function creerUtilisateur(donnees) {
const existant = await db.utilisateurs.findOne({ email: donnees.email });
if (existant) {
throw new ErreurConflict(`Email ${donnees.email} déjà utilisé`);
}
return db.utilisateurs.create(donnees);
}
async function trouverUtilisateur(id) {
const utilisateur = await db.utilisateurs.findById(id);
if (!utilisateur) {
throw new ErreurNonTrouve(`Utilisateur ${id}`);
}
return utilisateur;
}
Middleware de gestion d'erreurs Express
Un middleware centralisé évite de répéter la logique de réponse d'erreur dans chaque route.
// middleware/gererErreurs.js
import { ErreurApp } from '../errors/index.js';
export function gererErreurs(erreur, req, res, next) {
// Logger l'erreur
if (erreur.isOperational) {
logger.warn({
message: erreur.message,
code: erreur.code,
url: req.url,
method: req.method,
userId: req.utilisateur?.id
});
} else {
// Erreur non opérationnelle = potentiellement un bug
logger.error({
message: erreur.message,
stack: erreur.stack,
url: req.url,
method: req.method
});
}
// Réponse client
if (erreur instanceof ErreurApp) {
return res.status(erreur.statusCode).json({
erreur: {
message: erreur.message,
code: erreur.code,
...(erreur.champs && { champs: erreur.champs })
}
});
}
// Erreur inconnue — ne pas exposer les détails en production
const message = process.env.NODE_ENV === 'production'
? 'Une erreur interne est survenue'
: erreur.message;
res.status(500).json({
erreur: { message, code: 'INTERNAL_ERROR' }
});
}
// Dans app.js — doit être le dernier middleware
app.use(gererErreurs);
Gestion des erreurs async dans Express
Avant Express 5, les erreurs lancées dans des fonctions async ne sont pas automatiquement transmises au middleware d'erreurs. Il faut utiliser un wrapper.
// utils/asyncHandler.js
export function asyncHandler(fn) {
return function(req, res, next) {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
// Utilisation dans les routes
import { asyncHandler } from '../utils/asyncHandler.js';
router.get('/utilisateurs/:id', asyncHandler(async (req, res) => {
const utilisateur = await trouverUtilisateur(req.params.id);
res.json(utilisateur);
}));
// Si trouverUtilisateur lance ErreurNonTrouve, elle est transmise à gererErreurs
Avec Express 5 (actuellement en version stable), les fonctions async retournant une promesse rejetée sont automatiquement gérées — plus besoin du wrapper.
Gérer les unhandled rejections et uncaught exceptions
Deux types d'erreurs peuvent faire planter ton processus Node.js silencieusement si tu ne les gères pas.
// index.js — au démarrage de l'application
process.on('unhandledRejection', (raison, promesse) => {
logger.error({
message: 'Promesse rejetée non gérée',
raison: raison?.message || raison,
stack: raison?.stack
});
// En production, arrêter proprement le processus
// Un gestionnaire de processus (PM2, systemd) le redémarrera
process.exit(1);
});
process.on('uncaughtException', (erreur) => {
logger.error({
message: 'Exception non gérée',
erreur: erreur.message,
stack: erreur.stack
});
// Ne jamais continuer après une uncaughtException
// L'état de l'application est potentiellement corrompu
process.exit(1);
});
process.on('SIGTERM', () => {
logger.info('Signal SIGTERM reçu — arrêt gracieux');
serveur.close(() => {
logger.info('Serveur arrêté');
process.exit(0);
});
});
L'arrêt du processus après une uncaughtException peut sembler agressif, mais c'est la bonne décision. Node.js lui-même recommande cette approche dans sa documentation — un processus qui continue après une exception non gérée peut avoir un état corrompu et produire des comportements imprévisibles.
Logging structuré avec Pino
Les console.error en production, c'est bien intentionné mais insuffisant. Un logger structuré comme Pino produit du JSON que tu peux indexer et interroger.
import pino from 'pino';
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
transport: process.env.NODE_ENV === 'development'
? { target: 'pino-pretty' }
: undefined,
base: {
service: 'mon-api',
version: process.env.npm_package_version
}
});
// Utilisation
logger.info({ userId: 123, action: 'connexion' }, 'Utilisateur connecté');
logger.error({ erreur: err.message, stack: err.stack }, 'Erreur critique');
Le JSON produit par Pino est directement ingérable par des outils comme Datadog, Elastic Stack ou Grafana Loki. Beaucoup plus utile que du texte libre pour retrouver un bug en production.
Retry automatique avec backoff exponentiel
Pour les appels à des services externes, une stratégie de retry évite que des erreurs temporaires (rate limit, indisponibilité courte) deviennent des erreurs utilisateurs.
async function avecRetry(fn, options = {}) {
const { maxTentatives = 3, delaiBase = 1000, facteur = 2 } = options;
for (let tentative = 1; tentative <= maxTentatives; tentative++) {
try {
return await fn();
} catch (erreur) {
const estDerniereChance = tentative === maxTentatives;
const estRecuperable = erreur.statusCode >= 500 || erreur.code === 'ECONNRESET';
if (estDerniereChance || !estRecuperable) {
throw erreur;
}
const delai = delaiBase * Math.pow(facteur, tentative - 1);
const jitter = Math.random() * 200; // Éviter la synchronisation
logger.warn({ tentative, erreur: erreur.message, prochainRetryMs: delai + jitter });
await new Promise(resolve => setTimeout(resolve, delai + jitter));
}
}
}
// Utilisation
const donnees = await avecRetry(
() => serviceExterne.recuperer(id),
{ maxTentatives: 3, delaiBase: 500 }
);
Ce qu'on retient
Une gestion des erreurs solide repose sur quatre piliers : distinguer les erreurs opérationnelles des bugs, utiliser des classes d'erreurs structurées, centraliser la gestion dans un middleware, et logger de façon structurée.
La chose la plus importante : ne jamais ignorer une erreur silencieusement. Un .catch(() => {}) vide est l'une des pires pratiques qu'on voit en code Node.js — tu caches un problème qui te sautera à la figure plus tard, au pire moment.
Et pour les unhandledRejection : gère-les, log-les, et laisse le processus redémarrer. PM2 ou systemd sont là pour ça.
