AzDev

V - Gestion des erreurs

Dans une app Express.js, la gestion efficace des erreurs est un aspect fondamental qui détermine la robustesse, la maintenabilité et l'expérience utilisateur de votre service web. Express, avec son architecture basée sur les middlewares, offre une approche élégante et puissante pour intercepter, traiter et répondre aux différentes erreurs qui peuvent survenir lors de l'exécution de votre application.

Publié le
V - Gestion des erreurs

1. Introduction

1.1. Pourquoi la gestion des erreurs est cruciale dans Express

Express.js fonctionne sur un modèle de pipeline de middlewares où chaque requête traverse une série de fonctions avant de recevoir une réponse. Cette structure présente plusieurs défis spécifiques pour la gestion des erreurs :

  1. Propagation des erreurs : Sans une gestion appropriée, les erreurs peuvent interrompre le flux de traitement des requêtes et provoquer le crash complet de l'application.

  2. Contexte d'exécution asynchrone : Express étant basé sur Node.js, la plupart des opérations sont asynchrones (accès à la base de données, appels d'API externes, opérations de fichiers). Les erreurs dans du code asynchrone ne sont pas automatiquement capturées par les blocs try-catch traditionnels.

  3. Cohérence des réponses : Les utilisateurs et les systèmes clients s'attendent à recevoir des réponses formatées de manière cohérente, même en cas d'erreur.

  4. Sécurité des informations : Une gestion incorrecte des erreurs peut révéler des détails sensibles sur l'infrastructure ou la logique de votre application.

1.2. Avantages de la gestion centralisée des erreurs avec les middlewares

L'architecture middleware d'Express permet d'implémenter une stratégie de gestion des erreurs particulièrement efficace :

  1. Centralisation : Les middlewares d'erreur permettent de centraliser la logique de traitement des erreurs en un seul endroit, évitant la duplication de code.

  2. Séparation des préoccupations : Vous pouvez séparer clairement la logique métier du traitement des erreurs.

  3. Personnalisation : Express permet de créer différents middlewares d'erreur pour différents types d'erreurs (validation, authentification, autorisations, etc.).

  4. Flexibilité : Vous pouvez facilement adapter les réponses d'erreur selon l'environnement (développement vs production) ou le type de client (API vs navigateur web).

Un système de gestion d'erreurs bien conçu dans Express permet non seulement d'éviter les crashs d'application, mais aussi d'améliorer la qualité du code, de faciliter le débogage et d'offrir une meilleure expérience utilisateur. C'est pourquoi ce module se concentre sur les meilleures pratiques et techniques pour implémenter une gestion robuste des erreurs dans vos applications Express.

2. Stratégies de gestion d'erreurs

2.1. Erreurs synchrones vs asynchrones

La gestion des erreurs dans Express diffère selon que le code est synchrone ou asynchrone.

Gestion des erreurs synchrones

Express peut capturer automatiquement les erreurs synchrones qui se produisent dans les gestionnaires de route et les middlewares :

1app.get('/synchrone', (req, res) => {
2  // Cette erreur sera automatiquement capturée par Express
3  throw new Error('Erreur synchrone');
4});

Gestion des erreurs asynchrones

Les erreurs qui se produisent dans du code asynchrone doivent être explicitement transmises à Express via la fonction next() :

1app.get('/asynchrone', (req, res, next) => {
2  // Opération asynchrone (Promise)
3  fetchData()
4    .then(data => {
5      res.json(data);
6    })
7    .catch(err => {
8      // Transmission de l'erreur à Express
9      next(err);
10    });
11});
12
13// Avec async/await
14app.get('/async-await', async (req, res, next) => {
15  try {
16    const data = await fetchData();
17    res.json(data);
18  } catch (err) {
19    next(err);
20  }
21});

Si vous oubliez de gérer les erreurs dans du code asynchrone, elles ne seront pas capturées par Express et peuvent provoquer un crash de l'application.

2.3. Try/catch et promesses

Utilisation de try/catch

Pour le code synchrone ou avec async/await, utilisez try/catch :

1app.get('/utilisateurs/:id', async (req, res, next) => {
2  try {
3    // Validation
4    const id = parseInt(req.params.id);
5    if (isNaN(id)) {
6      throw new Error('ID invalide');
7    }
8    
9    // Opération asynchrone
10    const utilisateur = await trouverUtilisateur(id);
11    if (!utilisateur) {
12      // Erreur 404 personnalisée
13      const err = new Error('Utilisateur non trouvé');
14      err.statusCode = 404;
15      throw err;
16    }
17    
18    res.json(utilisateur);
19  } catch (err) {
20    // Transmission de l'erreur au middleware d'erreur
21    next(err);
22  }
23});

Utilisation des promesses

Pour les promesses sans async/await, utilisez .catch() :

1app.get('/produits/:id', (req, res, next) => {
2  const id = req.params.id;
3  
4  trouverProduit(id)
5    .then(produit => {
6      if (!produit) {
7        const err = new Error('Produit non trouvé');
8        err.statusCode = 404;
9        throw err; // Sera capturé par le .catch()
10      }
11      res.json(produit);
12    })
13    .catch(next); // Raccourci pour .catch(err => next(err))
14});

2.4. Middlewares d'erreur

Les middlewares d'erreur sont des fonctions spéciales qui prennent quatre arguments (err, req, res, next) et sont utilisés pour gérer les erreurs de manière centralisée.

1// Middleware d'erreur de base
2app.use((err, req, res, next) => {
3  console.error(err.stack);
4  res.status(500).send('Quelque chose s\'est mal passé!');
5});

Express reconnaît un middleware comme étant un middleware d'erreur par le nombre de ses arguments (4 au lieu de 3).

3. Implémentation de middlewares d'erreur

3.1. Structure d'un middleware d'erreur

Un middleware d'erreur complet devrait :

  1. Déterminer le code de statut HTTP approprié
  2. Formater une réponse d'erreur cohérente
  3. Journaliser l'erreur pour le débogage
  4. Gérer différemment les erreurs en développement et en production
1// middleware/errorHandler.js
2const errorHandler = (err, req, res, next) => {
3  // Déterminer le code de statut
4  const statusCode = err.statusCode || 500;
5  
6  // Préparer l'objet de réponse
7  const errorResponse = {
8    error: {
9      message: err.message || 'Erreur interne du serveur',
10      code: err.code || 'INTERNAL_ERROR'
11    }
12  };
13  
14  // En développement, ajouter des détails supplémentaires
15  if (process.env.NODE_ENV === 'development') {
16    errorResponse.error.stack = err.stack;
17    errorResponse.error.details = err.details || {};
18  }
19  
20  // Journaliser l'erreur
21  console.error(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
22  console.error(err.stack);
23  
24  // Envoyer la réponse
25  res.status(statusCode).json(errorResponse);
26};
27
28module.exports = errorHandler;

3.2. Gestion centralisée des erreurs

Pour une application plus grande, il est recommandé de créer une structure de gestion d'erreurs plus élaborée :

1// utils/errors.js
2class AppError extends Error {
3  constructor(message, statusCode, code, details = {}) {
4    super(message);
5    this.statusCode = statusCode;
6    this.code = code;
7    this.details = details;
8    this.isOperational = true; // Erreur opérationnelle vs programmation
9    
10    Error.captureStackTrace(this, this.constructor);
11  }
12}
13
14// Erreurs spécifiques
15class NotFoundError extends AppError {
16  constructor(message = 'Ressource non trouvée', details = {}) {
17    super(message, 404, 'RESOURCE_NOT_FOUND', details);
18  }
19}
20
21class ValidationError extends AppError {
22  constructor(message = 'Données invalides', details = {}) {
23    super(message, 400, 'VALIDATION_ERROR', details);
24  }
25}
26
27class UnauthorizedError extends AppError {
28  constructor(message = 'Non autorisé', details = {}) {
29    super(message, 401, 'UNAUTHORIZED', details);
30  }
31}
32
33class ForbiddenError extends AppError {
34  constructor(message = 'Accès interdit', details = {}) {
35    super(message, 403, 'FORBIDDEN', details);
36  }
37}
38
39module.exports = {
40  AppError,
41  NotFoundError,
42  ValidationError,
43  UnauthorizedError,
44  ForbiddenError
45};

Utilisation dans les routes :

1// routes/users.js
2const express = require('express');
3const router = express.Router();
4const { NotFoundError, ValidationError } = require('../utils/errors');
5
6router.get('/:id', async (req, res, next) => {
7  try {
8    const id = parseInt(req.params.id);
9    
10    if (isNaN(id)) {
11      throw new ValidationError('ID utilisateur invalide', { id: req.params.id });
12    }
13    
14    const user = await findUser(id);
15    
16    if (!user) {
17      throw new NotFoundError('Utilisateur non trouvé', { id });
18    }
19    
20    res.json(user);
21  } catch (err) {
22    next(err);
23  }
24});
25
26module.exports = router;

3.3. Personnalisation des messages d'erreur

Pour une meilleure expérience utilisateur, personnalisez les messages d'erreur en fonction du type d'erreur :

1// middleware/errorHandler.js
2const errorHandler = (err, req, res, next) => {
3  let statusCode = err.statusCode || 500;
4  let message = err.message || 'Erreur interne du serveur';
5  let errorCode = err.code || 'INTERNAL_ERROR';
6  
7  // Personnalisation en fonction du type d'erreur
8  if (err.name === 'ValidationError' && err.errors) {
9    // Erreur de validation Mongoose
10    statusCode = 400;
11    message = 'Données invalides';
12    errorCode = 'VALIDATION_ERROR';
13    
14    // Extraire les détails des erreurs de validation
15    const validationErrors = {};
16    for (const field in err.errors) {
17      validationErrors[field] = err.errors[field].message;
18    }
19    
20    err.details = validationErrors;
21  } else if (err.name === 'CastError' && err.kind === 'ObjectId') {
22    // Erreur de conversion d'ID MongoDB
23    statusCode = 400;
24    message = 'ID invalide';
25    errorCode = 'INVALID_ID';
26  } else if (err.code === 11000) {
27    // Erreur de duplication MongoDB
28    statusCode = 409;
29    message = 'Conflit de données';
30    errorCode = 'DUPLICATE_KEY';
31    
32    // Extraire la clé dupliquée
33    const field = Object.keys(err.keyValue)[0];
34    const value = err.keyValue[field];
35    err.details = { field, value };
36  }
37  
38  // Préparer la réponse
39  const errorResponse = {
40    error: {
41      message,
42      code: errorCode
43    }
44  };
45  
46  // Ajouter des détails en développement
47  if (process.env.NODE_ENV === 'development') {
48    errorResponse.error.stack = err.stack;
49    errorResponse.error.details = err.details || {};
50  } else if (err.details) {
51    // En production, inclure uniquement les détails pertinents
52    errorResponse.error.details = err.details;
53  }
54  
55  // Journaliser l'erreur
56  console.error(`[${new Date().toISOString()}] ${statusCode} ${errorCode}: ${message}`);
57  if (statusCode >= 500) {
58    console.error(err.stack);
59  }
60  
61  // Envoyer la réponse
62  res.status(statusCode).json(errorResponse);
63};
64
65module.exports = errorHandler;

4. Logging et monitoring

4.1. Implémentation de logging

Un bon système de logging est essentiel pour le débogage et la maintenance. Utilisez une bibliothèque comme Winston ou Pino plutôt que console.log() :

1// utils/logger.js
2const winston = require('winston');
3const path = require('path');
4
5// Définir les niveaux de log
6const levels = {
7  error: 0,
8  warn: 1,
9  info: 2,
10  http: 3,
11  debug: 4
12};
13
14// Déterminer le niveau en fonction de l'environnement
15const level = () => {
16  const env = process.env.NODE_ENV || 'development';
17  return env === 'development' ? 'debug' : 'warn';
18};
19
20// Définir les couleurs pour chaque niveau
21const colors = {
22  error: 'red',
23  warn: 'yellow',
24  info: 'green',
25  http: 'magenta',
26  debug: 'blue'
27};
28
29winston.addColors(colors);
30
31// Format pour la console
32const consoleFormat = winston.format.combine(
33  winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
34  winston.format.colorize({ all: true }),
35  winston.format.printf(
36    info => `${info.timestamp} ${info.level}: ${info.message}`
37  )
38);
39
40// Format pour les fichiers
41const fileFormat = winston.format.combine(
42  winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
43  winston.format.json()
44);
45
46// Définir les transports (destinations des logs)
47const transports = [
48  // Console
49  new winston.transports.Console({
50    format: consoleFormat
51  }),
52  // Fichier pour toutes les logs
53  new winston.transports.File({
54    filename: path.join('logs', 'all.log'),
55    format: fileFormat
56  }),
57  // Fichier séparé pour les erreurs
58  new winston.transports.File({
59    filename: path.join('logs', 'error.log'),
60    level: 'error',
61    format: fileFormat
62  })
63];
64
65// Créer le logger
66const logger = winston.createLogger({
67  level: level(),
68  levels,
69  transports
70});
71
72module.exports = logger;

Utilisation du logger :

1// app.js
2const express = require('express');
3const logger = require('./utils/logger');
4const morgan = require('morgan');
5const app = express();
6
7// Middleware de logging HTTP avec morgan qui utilise notre logger
8app.use(morgan('combined', {
9  stream: {
10    write: message => logger.http(message.trim())
11  }
12}));
13
14// Middleware d'erreur
15app.use((err, req, res, next) => {
16  logger.error(`${err.statusCode || 500} - ${err.message} - ${req.originalUrl} - ${req.method} - ${req.ip}`);
17  logger.error(err.stack);
18  
19  // Réponse d'erreur...
20});
21
22// Exemple d'utilisation dans une route
23app.get('/test-log', (req, res) => {
24  logger.debug('Message de débogage');
25  logger.info('Information importante');
26  logger.warn('Attention');
27  logger.error('Une erreur s\'est produite');
28  
29  res.send('Logs générés');
30});
31
32// Démarrage du serveur
33app.listen(3000, () => {
34  logger.info('Serveur démarré sur le port 3000');
35});

4.2. Outils de monitoring

Pour surveiller votre application en production, plusieurs outils sont disponibles :

PM2

PM2 est un gestionnaire de processus pour Node.js qui offre des fonctionnalités de monitoring :

1# Installation
2npm install pm2 -g
3
4# Démarrage de l'application
5pm2 start app.js --name "mon-app"
6
7# Monitoring en temps réel
8pm2 monit
9
10# Dashboard web
11pm2 plus

4.3. Bonnes pratiques pour la production

  1. Utiliser des niveaux de log appropriés :

    • ERROR : Erreurs qui nécessitent une intervention
    • WARN : Situations anormales mais non critiques
    • INFO : Événements normaux mais significatifs
    • DEBUG : Informations détaillées pour le débogage
  2. Structurer les logs :

    • Utiliser un format JSON pour faciliter l'analyse
    • Inclure des identifiants de corrélation pour suivre les requêtes
    • Ajouter des métadonnées utiles (utilisateur, IP, etc.)
  3. Rotation des logs :

    • Configurer la rotation des fichiers de log
    • Archiver les anciens logs
    • Définir une politique de rétention
  4. Centralisation des logs :

    • Utiliser un service comme ELK Stack (Elasticsearch, Logstash, Kibana)
    • Ou des services cloud comme AWS CloudWatch, Google Cloud Logging
  5. Alertes :

    • Configurer des alertes pour les erreurs critiques
  6. Sécurité des logs :

    • Ne pas logger d'informations sensibles (mots de passe, tokens)
    • Protéger l'accès aux fichiers de log

5. Exercices pratiques

Exercice 1: Implémenter un middleware de gestion d'erreurs centralisé

Créez un middleware de gestion d'erreurs qui :

  • Gère différents types d'erreurs (validation, authentification, etc.)
  • Formate les réponses d'erreur de manière cohérente
  • Journalise les erreurs avec différents niveaux de détail selon l'environnement

Exercice 2: Créer des pages d'erreur personnalisées

Créez une application Express qui affiche des pages d'erreur personnalisées pour différents codes HTTP (404, 500, etc.) avec un design cohérent avec le reste de l'application.

Exercice 3: Mettre en place un système de logging

Implémentez un système de logging complet avec Winston qui :

  • Écrit les logs dans des fichiers et la console
  • Utilise différents niveaux de log
  • Inclut des informations contextuelles (IP, utilisateur, etc.)
  • Configure la rotation des logs

6. Ressources supplémentaires