📗 Masterclass : Gestion des Erreurs en IngĂ©nierie Logicielle

Table des MatiĂšres

  1. Introduction Ă  la Gestion des Erreurs

  2. Pourquoi la Gestion des Erreurs est Cruciale

  3. Grands Principes de la Gestion des Erreurs

  4. Implémentation Frontend (React/Next.js)

  5. Implémentation Backend (NestJS)

  6. Analyse du Repo ASF Frontend

  7. ProblÚmes Identifiés et Solutions

  8. Bonnes Pratiques et Recommandations

  9. Exemples Pratiques et Patterns


1. Introduction Ă  la Gestion des Erreurs

Qu’est-ce que la Gestion des Erreurs ?

La gestion des erreurs est le processus de dĂ©tection, traitement et rĂ©cupĂ©ration des conditions exceptionnelles qui se produisent pendant l’exĂ©cution d’un programme. Elle englobe :

  • DĂ©tection : Identifier quand quelque chose ne va pas

  • Classification : CatĂ©goriser le type d’erreur

  • Notification : Informer les parties concernĂ©es (utilisateurs, dĂ©veloppeurs, systĂšmes)

  • RĂ©cupĂ©ration : Tenter de revenir Ă  un Ă©tat stable

  • Logging : Enregistrer l’erreur pour analyse ultĂ©rieure

Types d’Erreurs

1. Erreurs de Syntaxe

// Erreur détectée à la compilation
const x = 5;
const y = x.toUppercase(); // Erreur : toUppercase n'existe pas sur number

2. Erreurs d’ExĂ©cution (Runtime)

// Erreur détectée pendant l'exécution
const user = null;
console.log(user.name); // TypeError: Cannot read property 'name' of null

3. Erreurs Logiques

// Pas d'erreur technique, mais le résultat est incorrect
function calculateDiscount(price: number) {
  return price * 1.1; // Bug logique : on augmente au lieu de réduire
}

4. Erreurs RĂ©seau

// ProblĂšmes de communication avec des services externes
fetch('/api/users').catch(error => {
  // Connection refused, timeout, DNS error, etc.
});

5. Erreurs de Validation

// Données qui ne respectent pas les contraintes
const email = 'invalid-email'; // Ne respecte pas le format email
const age = -5; // Valeur invalide

6. Erreurs MĂ©tier

// Violations de rÚgles métier
class BankAccount {
  withdraw(amount: number) {
    if (this.balance < amount) {
      throw new InsufficientFundsError('Solde insuffisant');
    }
  }
}

2. Pourquoi c’est Important

2.1 ExpĂ©rience Utilisateur (UX)

Sans gestion d’erreurs :

[Écran blanc]

Avec gestion d’erreurs :

❌ Impossible de charger vos cours
ProblÚme de connexion réseau
[Bouton: Réessayer]

Impact :

  • RĂ©duction de la frustration utilisateur

  • Confiance dans l’application

  • ClartĂ© sur ce qui se passe

  • PossibilitĂ© de rĂ©cupĂ©ration

2.2 FiabilitĂ© du SystĂšme

Une application sans gestion d’erreurs peut :

  • Crasher complĂštement pour une erreur mineure

  • Perdre des donnĂ©es utilisateur

  • Rester dans un Ă©tat corrompu

  • Propager des erreurs en cascade

Exemple de propagation d’erreur :

// Sans gestion d'erreurs
async function processOrder(orderId: string) {
  const order = await getOrder(orderId); // ❌ Peut Ă©chouer
  const payment = await processPayment(order); // ❌ N'est jamais appelĂ© si getOrder Ă©choue
  await sendConfirmation(order); // ❌ N'est jamais appelĂ©
  return order;
}

// Avec gestion d'erreurs
async function processOrder(orderId: string) {
  try {
    const order = await getOrder(orderId);
    const payment = await processPayment(order);
    await sendConfirmation(order);
    return order;
  } catch (error) {
    // Log l'erreur
    ErrorLogger.log(error);

    // Rollback si nécessaire
    if (payment) {
      await refundPayment(payment);
    }

    // Notifier l'utilisateur
    throw new OrderProcessingError('Impossible de traiter la commande', { orderId });
  }
}

2.3 MaintenabilitĂ©ïƒ

Avantages pour les développeurs :

  1. Debugging facilité

    • Stack traces claires

    • Contexte de l’erreur

    • Logs structurĂ©s

  2. Monitoring et alertes

    • DĂ©tection proactive des problĂšmes

    • MĂ©triques d’erreurs

    • Tendances et patterns

  3. Documentation implicite

    // Le code documente les cas d'erreur possibles
    async function uploadFile(file: File) {
      if (file.size > MAX_SIZE) {
        throw new FileTooLargeError(`Max size: ${MAX_SIZE}`);
      }
      if (!ALLOWED_TYPES.includes(file.type)) {
        throw new InvalidFileTypeError(`Allowed: ${ALLOWED_TYPES.join(', ')}`);
      }
      // ...
    }
    

2.4 SĂ©curitĂ©ïƒ

Risques sans gestion d’erreurs appropriĂ©e :

  1. Information Disclosure

    // ❌ MAL : Exposer des dĂ©tails techniques
    catch (error) {
      res.status(500).json({
        error: error.stack, // RévÚle la structure interne
        query: sqlQuery,    // RĂ©vĂšle les requĂȘtes SQL
        config: dbConfig    // RévÚle les configurations
      });
    }
    
    // ✅ BIEN : Message gĂ©nĂ©rique en production
    catch (error) {
      logger.error(error); // Log en interne uniquement
      res.status(500).json({
        message: "Une erreur est survenue"
      });
    }
    
  2. SQL Injection via erreurs

    // Les messages d'erreur SQL peuvent révéler la structure de la DB
    // Il faut les masquer en production
    

2.5 CoĂ»t Business

Impact des erreurs non gérées :

  • Perte de revenus : Transactions Ă©chouĂ©es

  • Perte de clients : Mauvaise expĂ©rience

  • CoĂ»t de support : Tickets utilisateurs

  • RĂ©putation : Avis nĂ©gatifs, perte de confiance

  • Temps de dĂ©veloppement : Debugging difficile sans logs

Statistiques :

  • 88% des utilisateurs quittent une app aprĂšs une mauvaise expĂ©rience

  • Le coĂ»t moyen d’un incident en production : $5,600 par minute (source: Gartner)

  • 70% du temps de dĂ©veloppement est consacrĂ© au debugging


3. Grands Principes de la Gestion des Erreurs

3.1 Principe 1 : Fail Fast, Fail Loudly

Définition : Détecter et signaler les erreurs le plus tÎt possible.

// ❌ MAL : Retourner une valeur par dĂ©faut silencieusement
function getUser(id: string) {
  const user = database.findUser(id);
  return user || {}; // ❌ Cache le problùme
}

// ✅ BIEN : Échouer explicitement
function getUser(id: string) {
  const user = database.findUser(id);
  if (!user) {
    throw new UserNotFoundError(`User ${id} not found`);
  }
  return user;
}

Avantages :

  • Bugs dĂ©tectĂ©s immĂ©diatement

  • Pas de propagation de donnĂ©es corrompues

  • Stack trace au point exact de l’erreur

3.2 Principe 2 : Separation of Concerns

Définition : Séparer la détection, le traitement et la présentation des erreurs.

Architecture en couches :

┌─────────────────────────────────────┐
│   PrĂ©sentation (UI)                 │ ← Affiche Ă  l'utilisateur
├──────────────────────────────────────
│   Application (Business Logic)      │ ← Traite et transforme
├──────────────────────────────────────
│   Infrastructure (API, DB)          │ ← DĂ©tecte et lance
└─────────────────────────────────────┘

Exemple :

// Infrastructure Layer : Détection
class UserRepository {
  async findById(id: string) {
    try {
      const result = await db.query('SELECT * FROM users WHERE id = ?', [id]);
      if (!result) {
        throw new EntityNotFoundError('User', id);
      }
      return result;
    } catch (error) {
      if (error instanceof DatabaseError) {
        throw new RepositoryError('Failed to fetch user', { cause: error });
      }
      throw error;
    }
  }
}

// Application Layer : Traitement
class UserService {
  async getUserProfile(userId: string) {
    try {
      const user = await this.userRepository.findById(userId);
      return this.mapToProfileDTO(user);
    } catch (error) {
      if (error instanceof EntityNotFoundError) {
        // Log métrique
        metrics.increment('user.not_found');
        // Re-throw pour la couche supérieure
        throw new UserNotFoundError(`Profile not found for user ${userId}`);
      }
      // Log l'erreur inattendue
      logger.error('Failed to get user profile', { userId, error });
      throw new ServiceError('Unable to retrieve user profile');
    }
  }
}

// Presentation Layer : Affichage
class UserController {
  async getProfile(req, res) {
    try {
      const profile = await this.userService.getUserProfile(req.params.id);
      res.json(profile);
    } catch (error) {
      if (error instanceof UserNotFoundError) {
        res.status(404).json({
          messageFR: 'Utilisateur non trouvé',
          messageEN: 'User not found',
        });
      } else if (error instanceof ServiceError) {
        res.status(500).json({
          messageFR: 'Erreur lors de la récupération du profil',
          messageEN: 'Failed to retrieve profile',
        });
      } else {
        res.status(500).json({
          messageFR: 'Une erreur inattendue est survenue',
          messageEN: 'An unexpected error occurred',
        });
      }
    }
  }
}

3.3 Principe 3 : Error Classification

Définition : Catégoriser les erreurs pour un traitement approprié.

Catégories communes :

enum ErrorCategory {
  // Erreurs utilisateur (4xx)
  VALIDATION = 'VALIDATION', // 400, 422 - Données invalides
  AUTH = 'AUTH', // 401, 403 - Authentification/autorisation
  NOT_FOUND = 'NOT_FOUND', // 404 - Ressource inexistante
  CONFLICT = 'CONFLICT', // 409 - Conflit de ressource

  // Erreurs systĂšme (5xx)
  SERVER = 'SERVER', // 500 - Erreur serveur interne
  NETWORK = 'NETWORK', // - ProblĂšme de connexion
  TIMEOUT = 'TIMEOUT', // 408, 504 - Timeout
  UNAVAILABLE = 'UNAVAILABLE', // 503 - Service indisponible

  // Erreurs application
  RUNTIME = 'RUNTIME', // - Erreur d'exécution
  BUSINESS = 'BUSINESS', // - RÚgle métier violée
  UNKNOWN = 'UNKNOWN', // - Erreur non classifiée
}

enum ErrorSeverity {
  LOW = 'LOW', // Récupérable, impact minimal
  MEDIUM = 'MEDIUM', // Impact modéré, nécessite attention
  HIGH = 'HIGH', // Impact important, nécessite action
  FATAL = 'FATAL', // Application inutilisable
}

Exemple d’utilisation :

function classifyError(error: unknown): AppError {
  // Erreur HTTP
  if (error instanceof HttpError) {
    const status = error.status;

    if (status === 401 || status === 403) {
      return {
        category: ErrorCategory.AUTH,
        severity: ErrorSeverity.HIGH,
        message: 'Session expirée',
        recoverable: true,
        retryable: false,
      };
    }

    if (status >= 500) {
      return {
        category: ErrorCategory.SERVER,
        severity: ErrorSeverity.FATAL,
        message: 'Erreur serveur',
        recoverable: false,
        retryable: true,
      };
    }
  }

  // Erreur réseau
  if (error instanceof NetworkError) {
    return {
      category: ErrorCategory.NETWORK,
      severity: ErrorSeverity.MEDIUM,
      message: 'ProblĂšme de connexion',
      recoverable: true,
      retryable: true,
    };
  }

  // Erreur par défaut
  return {
    category: ErrorCategory.UNKNOWN,
    severity: ErrorSeverity.MEDIUM,
    message: 'Une erreur est survenue',
  };
}

3.4 Principe 4 : Graceful Degradation

DĂ©finition : L’application continue de fonctionner avec des capacitĂ©s rĂ©duites en cas d’erreur.

Exemples :

// 1. Fallback sur cache
async function getProducts() {
  try {
    return await api.getProducts();
  } catch (error) {
    logger.warn('API unavailable, using cache', error);
    return cache.getProducts(); // Données potentiellement anciennes
  }
}

// 2. Fonctionnalité optionnelle
async function loadDashboard() {
  const [userData, recommendationsResult, statisticsResult] = await Promise.allSettled([
    api.getUserData(),        // Critique
    api.getRecommendations(), // Optionnel
    api.getStatistics()       // Optionnel
  ]);

  if (userData.status === 'rejected') {
    throw new Error('Cannot load dashboard without user data');
  }

  return {
    user: userData.value,
    recommendations: recommendationsResult.status === 'fulfilled'
      ? recommendationsResult.value
      : [],
    statistics: statisticsResult.status === 'fulfilled'
      ? statisticsResult.value
      : null
  };
}

// 3. Mode dégradé d'UI
function UserAvatar({ userId }) {
  const { data: avatar, error } = useAvatar(userId);

  if (error) {
    return <DefaultAvatar />; // Fallback visuel
  }

  return <img src={avatar} />;
}

3.5 Principe 5 : Meaningful Error Messages

DĂ©finition : Messages clairs, actionnables et adaptĂ©s Ă  l’audience.

Pour les utilisateurs :

// ❌ MAL
'Error: ECONNREFUSED';
'Exception in thread main: NullPointerException';
'Error 500';

// ✅ BIEN
'Impossible de se connecter au serveur. Vérifiez votre connexion internet.';
"L'adresse email est déjà utilisée. Veuillez en choisir une autre.";
'Le service est temporairement indisponible. Réessayez dans quelques minutes.';

Pour les développeurs :

class AppError extends Error {
  constructor(
    message: string,
    public readonly code: string,
    public readonly context?: Record<string, unknown>
  ) {
    super(message);
    this.name = this.constructor.name;
  }

  toJSON() {
    return {
      name: this.name,
      message: this.message,
      code: this.code,
      context: this.context,
      stack: this.stack,
    };
  }
}

// Utilisation
throw new UserNotFoundError('User not found', 'USER_NOT_FOUND', { userId, requestId, timestamp });

3.6 Principe 6 : Error Logging & Monitoring

Définition : Enregistrer et surveiller les erreurs pour analyse et amélioration.

Niveaux de logging :

enum LogLevel {
  ERROR = 'error', // Erreurs nécessitant une action immédiate
  WARN = 'warn', // Situations anormales mais gérables
  INFO = 'info', // ÉvĂ©nements importants
  DEBUG = 'debug', // Détails pour debugging
}

// Exemple de logger structuré
class Logger {
  error(message: string, context?: Record<string, unknown>) {
    const log = {
      level: 'ERROR',
      timestamp: new Date().toISOString(),
      message,
      context,
      environment: process.env.NODE_ENV,
      version: process.env.APP_VERSION,
    };

    // Console en développement
    if (process.env.NODE_ENV === 'development') {
      console.error(log);
    }

    // Service de monitoring en production
    if (process.env.NODE_ENV === 'production') {
      Sentry.captureException(new Error(message), {
        extra: context,
      });
    }

    // Stockage local pour analyse
    this.persistToStorage(log);
  }
}

Métriques à surveiller :

  1. Taux d’erreur : Nombre d’erreurs / Total de requĂȘtes

  2. CatĂ©gories d’erreurs : Distribution par type

  3. Pages/endpoints affectĂ©s : OĂč se produisent les erreurs

  4. Tendances : Augmentation ou diminution

  5. Impact utilisateur : Nombre d’utilisateurs affectĂ©s

3.7 Principe 7 : Retry Logic

Définition : Réessayer automatiquement les opérations qui peuvent réussir aprÚs une tentative échouée.

Quand réessayer :

  • ✅ Erreurs rĂ©seau transitoires

  • ✅ Timeouts

  • ✅ Erreurs serveur 5xx (sauf 501)

  • ✅ Service temporairement indisponible (503)

  • ❌ Erreurs de validation (4xx)

  • ❌ Erreurs d’authentification (401, 403)

  • ❌ Ressource non trouvĂ©e (404)

Stratégies de retry :

// 1. Retry simple avec nombre de tentatives fixe
async function retrySimple<T>(fn: () => Promise<T>, maxRetries: number = 3): Promise<T> {
  let lastError: Error;

  for (let i = 0; i <= maxRetries; i++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error;
      if (i < maxRetries) {
        console.log(`Retry ${i + 1}/${maxRetries}`);
      }
    }
  }

  throw lastError;
}

// 2. Exponential Backoff (recommandé)
async function retryWithBackoff<T>(
  fn: () => Promise<T>,
  maxRetries: number = 3,
  baseDelay: number = 1000
): Promise<T> {
  let lastError: Error;

  for (let i = 0; i <= maxRetries; i++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error;

      if (i < maxRetries && isRetryable(error)) {
        const delay = baseDelay * Math.pow(2, i); // 1s, 2s, 4s, 8s...
        const jitter = Math.random() * 1000; // Ajoute du bruit
        await sleep(delay + jitter);
        console.log(`Retry ${i + 1}/${maxRetries} after ${delay}ms`);
      }
    }
  }

  throw lastError;
}

// 3. Retry avec conditions
async function retryWithCondition<T>(
  fn: () => Promise<T>,
  shouldRetry: (error: Error) => boolean,
  maxRetries: number = 3
): Promise<T> {
  let lastError: Error;

  for (let i = 0; i <= maxRetries; i++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error;

      if (i < maxRetries && shouldRetry(error)) {
        await sleep(1000 * Math.pow(2, i));
      } else {
        break;
      }
    }
  }

  throw lastError;
}

// Utilisation
const data = await retryWithBackoff(() => api.fetchData(), 3, 1000);

3.8 Principe 8 : Error Boundaries

DĂ©finition : Isoler les erreurs pour Ă©viter qu’elles ne crashent toute l’application.

Frontend (React) :

class ErrorBoundary extends React.Component<Props, State> {
  state = { hasError: false, error: null };

  static getDerivedStateFromError(error: Error) {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    // Log l'erreur
    ErrorLogger.log(error, {
      componentStack: errorInfo.componentStack
    });
  }

  render() {
    if (this.state.hasError) {
      return <ErrorFallback error={this.state.error} />;
    }
    return this.props.children;
  }
}

// Utilisation
<ErrorBoundary>
  <Dashboard />
</ErrorBoundary>

Backend (NestJS) :

// Exception Filter global
@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const request = ctx.getRequest();

    const error = this.normalizeError(exception);

    // Log l'erreur
    Logger.error(error.message, {
      path: request.url,
      method: request.method,
      stack: error.stack,
    });

    // Retourner réponse appropriée
    response.status(error.status).json({
      statusCode: error.status,
      messageFR: error.messageFR,
      messageEN: error.messageEN,
      timestamp: new Date().toISOString(),
      path: request.url,
    });
  }
}

4. ImplĂ©mentation Frontend (React/Next.js)

4.1 Architecture de Gestion des Erreurs

┌─────────────────────────────────────────────────┐
│  UI Layer (Components)                          │
│  - Affichage des erreurs                        │
│  - Error Boundaries                             │
│  - Toast notifications                          │
└────────────────┬────────────────────────────────┘
                 │
┌────────────────▌────────────────────────────────┐
│  Hook Layer (useQuery, useMutation)             │
│  - Gestion des Ă©tats de chargement/erreur       │
│  - Retry logic                                  │
│  - Cache invalidation                           │
└────────────────┬────────────────────────────────┘
                 │
┌────────────────▌────────────────────────────────┐
│  Service Layer                                  │
│  - Transformation des erreurs                   │
│  - Enrichissement du contexte                   │
│  - Mapping DTO                                  │
└────────────────┬────────────────────────────────┘
                 │
┌────────────────▌────────────────────────────────┐
│  HTTP Client Layer                              │
│  - Intercepteurs                                │
│  - Gestion des tokens                           │
│  - Retry automatique                            │
└────────────────┬────────────────────────────────┘
                 │
┌────────────────▌────────────────────────────────┐
│  Error Utils (Classification, Logging)          │
│  - Classification des erreurs                   │
│  - Logging structurĂ©                            │
│  - Monitoring (Sentry, etc.)                    │
└─────────────────────────────────────────────────┘

4.2 HTTP Client avec Gestion d’Erreurs

// httpClient.ts
import axios, { AxiosInstance, AxiosError } from 'axios';

export class HttpError extends Error {
  constructor(
    message: string,
    public readonly status: number,
    public readonly data?: unknown
  ) {
    super(message);
    this.name = 'HttpError';
  }
}

export class HttpClient {
  private client: AxiosInstance;
  private isRefreshing = false;
  private failedQueue: Array<{
    resolve: (value: unknown) => void;
    reject: (error: unknown) => void;
  }> = [];

  constructor(baseURL: string) {
    this.client = axios.create({
      baseURL,
      timeout: 30000, // 30 secondes
      headers: {
        'Content-Type': 'application/json',
      },
    });

    this.setupInterceptors();
  }

  private setupInterceptors() {
    // Request interceptor
    this.client.interceptors.request.use(
      config => {
        const token = this.getAccessToken();
        if (token) {
          config.headers.Authorization = `Bearer ${token}`;
        }
        return config;
      },
      error => Promise.reject(error)
    );

    // Response interceptor
    this.client.interceptors.response.use(
      response => response,
      async (error: AxiosError) => {
        const originalRequest = error.config;

        // Gestion du refresh token
        if (error.response?.status === 401 && !originalRequest._retry) {
          if (this.isRefreshing) {
            return new Promise((resolve, reject) => {
              this.failedQueue.push({ resolve, reject });
            });
          }

          originalRequest._retry = true;
          this.isRefreshing = true;

          try {
            const newToken = await this.refreshAccessToken();
            this.processQueue(null, newToken);
            originalRequest.headers.Authorization = `Bearer ${newToken}`;
            return this.client(originalRequest);
          } catch (refreshError) {
            this.processQueue(refreshError, null);
            this.clearTokens();
            return Promise.reject(refreshError);
          } finally {
            this.isRefreshing = false;
          }
        }

        // Transformer en HttpError
        return Promise.reject(this.transformError(error));
      }
    );
  }

  private transformError(error: AxiosError): HttpError {
    if (error.response) {
      // Le serveur a répondu avec un statut d'erreur
      return new HttpError(error.message, error.response.status, error.response.data);
    } else if (error.request) {
      // La requĂȘte a Ă©tĂ© envoyĂ©e mais pas de rĂ©ponse
      return new HttpError('ProblÚme de connexion réseau', 0, { originalError: error });
    } else {
      // Erreur lors de la configuration de la requĂȘte
      return new HttpError(error.message, 0, { originalError: error });
    }
  }

  async get<T>(url: string): Promise<T> {
    const response = await this.client.get<T>(url);
    return response.data;
  }

  async post<T>(url: string, data?: unknown): Promise<T> {
    const response = await this.client.post<T>(url, data);
    return response.data;
  }

  // ... autres méthodes
}

4.3 Service Layer avec Error Handling

// coursesService.ts
export class CoursesService extends HttpClient {
  async getAllCourses(): Promise<Course[]> {
    try {
      const response = await this.get<ApiCourseModel[]>('/cours');

      if (!response || !Array.isArray(response)) {
        throw new Error('Invalid response format');
      }

      return response.map(course => CoursesMapper.mapApiCourseToCourseEntity(course));
    } catch (error) {
      // Classifier l'erreur
      const classifiedError = classifyError(error);

      // Logger avec contexte
      ErrorLogger.log(classifiedError, {
        service: 'CoursesService',
        method: 'getAllCourses',
        timestamp: new Date().toISOString(),
      });

      // Re-throw pour que les hooks puissent gérer
      throw classifiedError;
    }
  }

  async getCourseById(id: string): Promise<Course> {
    try {
      const response = await this.get<ApiCourseModel>(`/cours/${id}`);

      if (!response) {
        throw new NotFoundError(`Course with id ${id} not found`);
      }

      return CoursesMapper.mapApiCourseToCourseEntity(response);
    } catch (error) {
      if (error instanceof HttpError && error.status === 404) {
        throw new NotFoundError(`Course ${id} not found`);
      }

      const classifiedError = classifyError(error);
      ErrorLogger.log(classifiedError, {
        service: 'CoursesService',
        method: 'getCourseById',
        courseId: id,
      });

      throw classifiedError;
    }
  }
}

4.4 React Query Hooks avec Error Handling

// useCourses.ts
export function useCourses() {
  const queryClient = useQueryClient();

  return useQuery({
    queryKey: ['courses'],
    queryFn: () => coursesService.getAllCourses(),
    retry: (failureCount, error) => {
      // Ne pas retry les erreurs client (4xx)
      if (error instanceof HttpError && error.status >= 400 && error.status < 500) {
        return false;
      }
      // Retry jusqu'Ă  3 fois pour les erreurs serveur
      return failureCount < 3;
    },
    retryDelay: attemptIndex => {
      // Exponential backoff: 1s, 2s, 4s
      return Math.min(1000 * 2 ** attemptIndex, 30000);
    },
    staleTime: 5 * 60 * 1000, // 5 minutes
    onError: error => {
      const classified = classifyError(error);

      // Log l'erreur
      ErrorLogger.log(classified, {
        hook: 'useCourses',
        action: 'fetch',
      });

      // Notifier l'utilisateur selon la sévérité
      if (
        classified.severity === ErrorSeverity.HIGH ||
        classified.severity === ErrorSeverity.FATAL
      ) {
        toastService.error(
          {
            title: 'Erreur de chargement',
            fallbackDescription: 'Impossible de charger les cours',
          },
          error
        );
      }
    },
  });
}

// useEnrollCourse.ts (Mutation)
export function useEnrollCourse() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (courseId: string) => coursesService.enrollCourse(courseId),
    onSuccess: (data, courseId) => {
      // Invalider les caches
      queryClient.invalidateQueries({ queryKey: ['courses'] });
      queryClient.invalidateQueries({ queryKey: ['enrolled-courses'] });

      // Notifier succĂšs
      toastService.success({
        title: 'Inscription réussie',
        description: 'Vous ĂȘtes maintenant inscrit au cours',
      });
    },
    onError: (error, courseId) => {
      const classified = classifyError(error);

      // Logger
      ErrorLogger.log(classified, {
        hook: 'useEnrollCourse',
        action: 'enroll',
        courseId,
      });

      // Notifier l'utilisateur avec message approprié
      if (classified.category === ErrorCategory.AUTH) {
        toastService.error(
          {
            title: 'Session expirée',
            fallbackDescription: 'Veuillez vous reconnecter',
          },
          error
        );
      } else if (classified.category === ErrorCategory.CONFLICT) {
        toastService.error(
          {
            title: 'Déjà inscrit',
            fallbackDescription: 'Vous ĂȘtes dĂ©jĂ  inscrit Ă  ce cours',
          },
          error
        );
      } else {
        toastService.error(
          {
            title: "Erreur d'inscription",
            fallbackDescription: 'Impossible de vous inscrire au cours',
          },
          error
        );
      }
    },
    // Pas de retry pour les mutations par défaut
    retry: false,
  });
}

4.5 Components avec Error Display

// CourseList.tsx
export function CourseList() {
  const {
    data: courses,
    isLoading,
    error,
    refetch
  } = useCourses();

  // Afficher le loading
  if (isLoading) {
    return <CoursesLoadingSkeleton />;
  }

  // Afficher l'erreur si présente
  if (error) {
    const classified = classifyError(error);

    return (
      <ErrorDisplay
        title="Impossible de charger les cours"
        message={classified.message}
        severity={classified.severity}
        onRetry={classified.retryable ? refetch : undefined}
      />
    );
  }

  // Afficher le contenu
  if (!courses || courses.length === 0) {
    return <EmptyState message="Aucun cours disponible" />;
  }

  return (
    <div className="grid grid-cols-3 gap-4">
      {courses.map(course => (
        <CourseCard key={course.id} course={course} />
      ))}
    </div>
  );
}

// ErrorDisplay.tsx
interface ErrorDisplayProps {
  title: string;
  message: string;
  severity: ErrorSeverity;
  onRetry?: () => void;
}

export function ErrorDisplay({ title, message, severity, onRetry }: ErrorDisplayProps) {
  const Icon = getIconBySeverity(severity);
  const color = getColorBySeverity(severity);

  return (
    <div className={`border-l-4 p-4 ${color.border} ${color.bg}`}>
      <div className="flex items-start">
        <Icon className={`h-5 w-5 ${color.text}`} />
        <div className="ml-3">
          <h3 className={`text-sm font-medium ${color.text}`}>
            {title}
          </h3>
          <p className="mt-2 text-sm text-gray-700">
            {message}
          </p>
          {onRetry && (
            <button
              onClick={onRetry}
              className="mt-3 text-sm font-medium text-blue-600 hover:text-blue-500"
            >
              Réessayer
            </button>
          )}
        </div>
      </div>
    </div>
  );
}

function getColorBySeverity(severity: ErrorSeverity) {
  switch (severity) {
    case ErrorSeverity.FATAL:
      return {
        border: 'border-red-500',
        bg: 'bg-red-50',
        text: 'text-red-800'
      };
    case ErrorSeverity.HIGH:
      return {
        border: 'border-orange-500',
        bg: 'bg-orange-50',
        text: 'text-orange-800'
      };
    case ErrorSeverity.MEDIUM:
      return {
        border: 'border-yellow-500',
        bg: 'bg-yellow-50',
        text: 'text-yellow-800'
      };
    case ErrorSeverity.LOW:
      return {
        border: 'border-blue-500',
        bg: 'bg-blue-50',
        text: 'text-blue-800'
      };
  }
}

4.6 Form Error Handling avec React Hook Form

// LoginForm.tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

const loginSchema = z.object({
  email: z.string().email('Email invalide'),
  password: z.string().min(6, 'Minimum 6 caractĂšres')
});

type LoginFormData = z.infer<typeof loginSchema>;

export function LoginForm() {
  const { register, handleSubmit, formState: { errors }, setError } = useForm<LoginFormData>({
    resolver: zodResolver(loginSchema)
  });

  const { mutate: login, isLoading } = useLogin();

  const onSubmit = async (data: LoginFormData) => {
    try {
      await login(data);
    } catch (error) {
      // Gérer les erreurs de validation backend
      if (error instanceof HttpError && error.status === 422) {
        const validationErrors = extractValidationErrors(error);

        // Mapper les erreurs aux champs du formulaire
        if (validationErrors) {
          Object.entries(validationErrors).forEach(([field, messages]) => {
            setError(field as keyof LoginFormData, {
              type: 'server',
              message: messages[0]
            });
          });
        }
      }
    }
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <label>Email</label>
        <input {...register('email')} />
        {errors.email && (
          <FormErrorMessage>{errors.email.message}</FormErrorMessage>
        )}
      </div>

      <div>
        <label>Mot de passe</label>
        <input type="password" {...register('password')} />
        {errors.password && (
          <FormErrorMessage>{errors.password.message}</FormErrorMessage>
        )}
      </div>

      <button type="submit" disabled={isLoading}>
        {isLoading ? 'Connexion...' : 'Se connecter'}
      </button>
    </form>
  );
}

// FormErrorMessage.tsx
export function FormErrorMessage({ children }: { children: React.ReactNode }) {
  return (
    <p className="mt-1 text-sm text-red-600 flex items-center gap-1">
      <AlertCircle className="h-4 w-4" />
      {children}
    </p>
  );
}

4.7 Error Boundaries

// ErrorBoundary.tsx
import React, { Component, ErrorInfo, ReactNode } from 'react';

interface Props {
  children: ReactNode;
  fallback?: (error: Error) => ReactNode;
  onError?: (error: Error, errorInfo: ErrorInfo) => void;
}

interface State {
  hasError: boolean;
  error: Error | null;
}

export class ErrorBoundary extends Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error: Error): State {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    // Classifier l'erreur
    const classified = classifyError(error);

    // Logger
    ErrorLogger.log(classified, {
      componentStack: errorInfo.componentStack,
      boundary: 'ErrorBoundary'
    });

    // Callback custom
    this.props.onError?.(error, errorInfo);
  }

  render() {
    if (this.state.hasError && this.state.error) {
      if (this.props.fallback) {
        return this.props.fallback(this.state.error);
      }

      return <DefaultErrorFallback error={this.state.error} />;
    }

    return this.props.children;
  }
}

// DefaultErrorFallback.tsx
function DefaultErrorFallback({ error }: { error: Error }) {
  const classified = classifyError(error);

  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-50">
      <div className="max-w-md w-full bg-white shadow-lg rounded-lg p-6">
        <div className="flex items-center justify-center w-12 h-12 mx-auto bg-red-100 rounded-full">
          <XCircle className="h-6 w-6 text-red-600" />
        </div>

        <h2 className="mt-4 text-center text-xl font-semibold text-gray-900">
          Une erreur est survenue
        </h2>

        <p className="mt-2 text-center text-sm text-gray-600">
          {classified.message}
        </p>

        {process.env.NODE_ENV === 'development' && (
          <details className="mt-4">
            <summary className="cursor-pointer text-sm text-gray-500">
              Détails techniques
            </summary>
            <pre className="mt-2 text-xs bg-gray-100 p-2 rounded overflow-auto">
              {error.stack}
            </pre>
          </details>
        )}

        <div className="mt-6 flex gap-3">
          <button
            onClick={() => window.location.reload()}
            className="flex-1 bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
          >
            Recharger la page
          </button>
          <button
            onClick={() => window.history.back()}
            className="flex-1 bg-gray-200 text-gray-700 px-4 py-2 rounded hover:bg-gray-300"
          >
            Retour
          </button>
        </div>
      </div>
    </div>
  );
}

// Utilisation dans l'app
// app/layout.tsx
export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html>
      <body>
        <ErrorBoundary>
          <QueryClientProvider client={queryClient}>
            {children}
          </QueryClientProvider>
        </ErrorBoundary>
      </body>
    </html>
  );
}

// app/dashboard/layout.tsx
export default function DashboardLayout({ children }: { children: ReactNode }) {
  const router = useRouter();

  return (
    <ErrorBoundary
      onError={(error) => {
        const classified = classifyError(error);

        // Rediriger vers login si erreur d'auth
        if (classified.category === ErrorCategory.AUTH) {
          router.push('/auth/login');
        }
      }}
    >
      {children}
    </ErrorBoundary>
  );
}

5. ImplĂ©mentation Backend (NestJS)

5.1 Architecture de Gestion des Erreurs Backend

┌──────────────────────────────────────────────────┐
│  Controller Layer                                │
│  - Validation des inputs (DTOs)                  │
│  - Exception filters                             │
└────────────────┬─────────────────────────────────┘
                 │
┌────────────────▌─────────────────────────────────┐
│  Service Layer (Business Logic)                  │
│  - Logique mĂ©tier                                │
│  - Lancement d'exceptions custom                 │
└────────────────┬─────────────────────────────────┘
                 │
┌────────────────▌─────────────────────────────────┐
│  Repository Layer                                │
│  - AccĂšs base de donnĂ©es                         │
│  - Gestion des erreurs DB                        │
└────────────────┬─────────────────────────────────┘
                 │
┌────────────────▌─────────────────────────────────┐
│  Global Exception Filter                         │
│  - Catch toutes les exceptions                   │
│  - Formatage des rĂ©ponses                        │
│  - Logging                                       │
└──────────────────────────────────────────────────┘

5.2 Custom Exceptions

// exceptions/base.exception.ts
export abstract class BaseException extends Error {
  constructor(
    public readonly message: string,
    public readonly messageFR: string,
    public readonly messageEN: string,
    public readonly statusCode: number,
    public readonly errorCode: string,
    public readonly context?: Record<string, unknown>
  ) {
    super(message);
    this.name = this.constructor.name;
    Error.captureStackTrace(this, this.constructor);
  }

  toJSON() {
    return {
      name: this.name,
      message: this.message,
      messageFR: this.messageFR,
      messageEN: this.messageEN,
      statusCode: this.statusCode,
      errorCode: this.errorCode,
      context: this.context,
      timestamp: new Date().toISOString(),
    };
  }
}

// exceptions/business.exception.ts
export class BusinessException extends BaseException {
  constructor(
    messageFR: string,
    messageEN: string,
    errorCode: string,
    context?: Record<string, unknown>
  ) {
    super(
      messageFR, // Message par défaut en FR
      messageFR,
      messageEN,
      400, // Bad Request
      errorCode,
      context
    );
  }
}

// exceptions/not-found.exception.ts
export class NotFoundException extends BaseException {
  constructor(resource: string, id: string | number, context?: Record<string, unknown>) {
    const messageFR = `${resource} avec l'identifiant ${id} introuvable`;
    const messageEN = `${resource} with id ${id} not found`;

    super(messageFR, messageFR, messageEN, 404, 'RESOURCE_NOT_FOUND', { resource, id, ...context });
  }
}

// exceptions/validation.exception.ts
export class ValidationException extends BaseException {
  constructor(
    public readonly errors: Record<string, string[]>,
    context?: Record<string, unknown>
  ) {
    super(
      'Erreur de validation',
      'Les données fournies sont invalides',
      'Validation failed',
      422, // Unprocessable Entity
      'VALIDATION_ERROR',
      { errors, ...context }
    );
  }
}

// exceptions/auth.exception.ts
export class UnauthorizedException extends BaseException {
  constructor(reason?: string, context?: Record<string, unknown>) {
    super(
      reason || 'Non authentifié',
      'Vous devez ĂȘtre connectĂ© pour accĂ©der Ă  cette ressource',
      'You must be authenticated to access this resource',
      401,
      'UNAUTHORIZED',
      context
    );
  }
}

export class ForbiddenException extends BaseException {
  constructor(reason?: string, context?: Record<string, unknown>) {
    super(
      reason || 'AccĂšs interdit',
      "Vous n'avez pas les permissions nécessaires",
      "You don't have permission to access this resource",
      403,
      'FORBIDDEN',
      context
    );
  }
}

// exceptions/conflict.exception.ts
export class ConflictException extends BaseException {
  constructor(messageFR: string, messageEN: string, context?: Record<string, unknown>) {
    super(messageFR, messageFR, messageEN, 409, 'CONFLICT', context);
  }
}

5.3 Global Exception Filter

// filters/global-exception.filter.ts
import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
  Logger,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { QueryFailedError } from 'typeorm';

@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
  private readonly logger = new Logger(GlobalExceptionFilter.name);

  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();

    const errorResponse = this.buildErrorResponse(exception, request);

    // Logger l'erreur
    this.logError(exception, request, errorResponse);

    // Envoyer la réponse
    response.status(errorResponse.statusCode).json(errorResponse);
  }

  private buildErrorResponse(exception: unknown, request: Request) {
    const timestamp = new Date().toISOString();
    const path = request.url;
    const method = request.method;

    // Exception custom
    if (exception instanceof BaseException) {
      return {
        statusCode: exception.statusCode,
        messageFR: exception.messageFR,
        messageEN: exception.messageEN,
        errorCode: exception.errorCode,
        context: exception.context,
        timestamp,
        path,
        method,
      };
    }

    // HttpException de NestJS
    if (exception instanceof HttpException) {
      const status = exception.getStatus();
      const exceptionResponse = exception.getResponse();

      return {
        statusCode: status,
        messageFR: this.extractMessage(exceptionResponse),
        messageEN: this.extractMessage(exceptionResponse),
        errorCode: this.getErrorCodeFromStatus(status),
        timestamp,
        path,
        method,
      };
    }

    // Erreur TypeORM
    if (exception instanceof QueryFailedError) {
      return this.handleDatabaseError(exception, timestamp, path, method);
    }

    // Erreur inconnue
    return {
      statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
      messageFR: 'Une erreur interne est survenue',
      messageEN: 'An internal server error occurred',
      errorCode: 'INTERNAL_ERROR',
      timestamp,
      path,
      method,
      // Seulement en développement
      ...(process.env.NODE_ENV === 'development' && {
        details: exception instanceof Error ? exception.message : String(exception),
        stack: exception instanceof Error ? exception.stack : undefined,
      }),
    };
  }

  private handleDatabaseError(
    error: QueryFailedError,
    timestamp: string,
    path: string,
    method: string
  ) {
    const driverError = error.driverError;

    // Contrainte unique violée
    if (driverError.code === '23505') {
      return {
        statusCode: HttpStatus.CONFLICT,
        messageFR: 'Cette ressource existe déjà',
        messageEN: 'This resource already exists',
        errorCode: 'DUPLICATE_ENTRY',
        timestamp,
        path,
        method,
      };
    }

    // Contrainte de clé étrangÚre violée
    if (driverError.code === '23503') {
      return {
        statusCode: HttpStatus.BAD_REQUEST,
        messageFR: 'Référence invalide',
        messageEN: 'Invalid reference',
        errorCode: 'FOREIGN_KEY_VIOLATION',
        timestamp,
        path,
        method,
      };
    }

    // Erreur DB générique
    return {
      statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
      messageFR: 'Erreur de base de données',
      messageEN: 'Database error',
      errorCode: 'DATABASE_ERROR',
      timestamp,
      path,
      method,
    };
  }

  private logError(exception: unknown, request: Request, errorResponse: any) {
    const { statusCode, errorCode, messageFR } = errorResponse;
    const { method, url, ip } = request;

    const context = {
      statusCode,
      errorCode,
      method,
      url,
      ip,
      userAgent: request.get('user-agent'),
      userId: request['user']?.id,
    };

    // Log selon la sévérité
    if (statusCode >= 500) {
      this.logger.error(
        `${method} ${url} - ${errorCode}: ${messageFR}`,
        exception instanceof Error ? exception.stack : undefined,
        JSON.stringify(context)
      );
    } else if (statusCode >= 400) {
      this.logger.warn(`${method} ${url} - ${errorCode}: ${messageFR}`, JSON.stringify(context));
    } else {
      this.logger.log(`${method} ${url} - ${errorCode}: ${messageFR}`, JSON.stringify(context));
    }

    // Envoyer Ă  Sentry en production
    if (process.env.NODE_ENV === 'production' && statusCode >= 500) {
      // Sentry.captureException(exception, { extra: context });
    }
  }

  private extractMessage(response: string | object): string {
    if (typeof response === 'string') {
      return response;
    }
    if (typeof response === 'object' && 'message' in response) {
      return Array.isArray(response.message) ? response.message[0] : response.message;
    }
    return 'Une erreur est survenue';
  }

  private getErrorCodeFromStatus(status: number): string {
    const codes = {
      400: 'BAD_REQUEST',
      401: 'UNAUTHORIZED',
      403: 'FORBIDDEN',
      404: 'NOT_FOUND',
      409: 'CONFLICT',
      422: 'VALIDATION_ERROR',
      500: 'INTERNAL_ERROR',
      503: 'SERVICE_UNAVAILABLE',
    };
    return codes[status] || 'UNKNOWN_ERROR';
  }
}

5.4 Validation avec DTOs

// dtos/create-course.dto.ts
import {
  IsString,
  IsNotEmpty,
  IsEnum,
  IsNumber,
  Min,
  Max,
  IsOptional,
  IsUrl,
} from 'class-validator';

export enum CourseLevel {
  BEGINNER = 'BEGINNER',
  INTERMEDIATE = 'INTERMEDIATE',
  ADVANCED = 'ADVANCED',
}

export class CreateCourseDto {
  @IsString({ message: 'Le titre doit ĂȘtre une chaĂźne de caractĂšres' })
  @IsNotEmpty({ message: 'Le titre est obligatoire' })
  title: string;

  @IsString({ message: 'La description doit ĂȘtre une chaĂźne de caractĂšres' })
  @IsNotEmpty({ message: 'La description est obligatoire' })
  description: string;

  @IsEnum(CourseLevel, { message: 'Niveau invalide' })
  level: CourseLevel;

  @IsNumber({}, { message: 'La durĂ©e doit ĂȘtre un nombre' })
  @Min(1, { message: 'La durée minimale est de 1 heure' })
  @Max(1000, { message: 'La durée maximale est de 1000 heures' })
  duration: number;

  @IsOptional()
  @IsUrl({}, { message: "L'URL de l'image est invalide" })
  imageUrl?: string;
}

// pipes/validation.pipe.ts
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToClass } from 'class-transformer';

@Injectable()
export class ValidationPipe implements PipeTransform<any> {
  async transform(value: any, { metatype }: ArgumentMetadata) {
    if (!metatype || !this.toValidate(metatype)) {
      return value;
    }

    const object = plainToClass(metatype, value);
    const errors = await validate(object);

    if (errors.length > 0) {
      const formattedErrors = this.formatErrors(errors);
      throw new ValidationException(formattedErrors);
    }

    return value;
  }

  private toValidate(metatype: Function): boolean {
    const types: Function[] = [String, Boolean, Number, Array, Object];
    return !types.includes(metatype);
  }

  private formatErrors(errors: any[]): Record<string, string[]> {
    const formatted: Record<string, string[]> = {};

    errors.forEach(error => {
      formatted[error.property] = Object.values(error.constraints);
    });

    return formatted;
  }
}

5.5 Service Layer avec Error Handling

// courses/courses.service.ts
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';

@Injectable()
export class CoursesService {
  private readonly logger = new Logger(CoursesService.name);

  constructor(
    @InjectRepository(Course)
    private coursesRepository: Repository<Course>
  ) {}

  async findAll(): Promise<Course[]> {
    try {
      return await this.coursesRepository.find();
    } catch (error) {
      this.logger.error('Failed to fetch courses', error.stack);
      throw new BusinessException(
        'Erreur lors de la récupération des cours',
        'Failed to fetch courses',
        'COURSES_FETCH_ERROR'
      );
    }
  }

  async findOne(id: string): Promise<Course> {
    const course = await this.coursesRepository.findOne({ where: { id } });

    if (!course) {
      throw new NotFoundException('Course', id);
    }

    return course;
  }

  async create(createCourseDto: CreateCourseDto): Promise<Course> {
    try {
      // Vérifier si le cours existe déjà
      const existing = await this.coursesRepository.findOne({
        where: { title: createCourseDto.title },
      });

      if (existing) {
        throw new ConflictException(
          'Un cours avec ce titre existe déjà',
          'A course with this title already exists',
          { title: createCourseDto.title }
        );
      }

      const course = this.coursesRepository.create(createCourseDto);
      return await this.coursesRepository.save(course);
    } catch (error) {
      if (error instanceof BaseException) {
        throw error;
      }

      this.logger.error('Failed to create course', error.stack);
      throw new BusinessException(
        'Erreur lors de la création du cours',
        'Failed to create course',
        'COURSE_CREATE_ERROR',
        { dto: createCourseDto }
      );
    }
  }

  async enroll(userId: string, courseId: string): Promise<void> {
    // Vérifier que le cours existe
    const course = await this.findOne(courseId);

    // Vérifier que l'utilisateur n'est pas déjà inscrit
    const existingEnrollment = await this.enrollmentsRepository.findOne({
      where: { userId, courseId },
    });

    if (existingEnrollment) {
      throw new ConflictException(
        'Vous ĂȘtes dĂ©jĂ  inscrit Ă  ce cours',
        'You are already enrolled in this course',
        { userId, courseId }
      );
    }

    // Vérifier que le cours n'est pas complet
    const enrollmentCount = await this.enrollmentsRepository.count({
      where: { courseId },
    });

    if (enrollmentCount >= course.maxStudents) {
      throw new BusinessException('Ce cours est complet', 'This course is full', 'COURSE_FULL', {
        courseId,
        maxStudents: course.maxStudents,
      });
    }

    // Créer l'inscription
    try {
      const enrollment = this.enrollmentsRepository.create({
        userId,
        courseId,
        enrolledAt: new Date(),
      });

      await this.enrollmentsRepository.save(enrollment);
    } catch (error) {
      this.logger.error('Failed to enroll user', error.stack);
      throw new BusinessException(
        "Erreur lors de l'inscription",
        'Failed to enroll',
        'ENROLLMENT_ERROR',
        { userId, courseId }
      );
    }
  }
}

5.6 Controller avec Error Handling

// courses/courses.controller.ts
import {
  Controller,
  Get,
  Post,
  Body,
  Param,
  UseGuards,
  HttpCode,
  HttpStatus,
} from '@nestjs/common';

@Controller('cours')
export class CoursesController {
  constructor(private readonly coursesService: CoursesService) {}

  @Get()
  async findAll() {
    try {
      return await this.coursesService.findAll();
    } catch (error) {
      // L'erreur sera catchée par le GlobalExceptionFilter
      throw error;
    }
  }

  @Get(':id')
  async findOne(@Param('id') id: string) {
    return await this.coursesService.findOne(id);
  }

  @Post()
  @UseGuards(JwtAuthGuard, RolesGuard)
  @Roles(Role.ADMIN)
  @HttpCode(HttpStatus.CREATED)
  async create(@Body(ValidationPipe) createCourseDto: CreateCourseDto) {
    return await this.coursesService.create(createCourseDto);
  }

  @Post(':id/enroll')
  @UseGuards(JwtAuthGuard)
  @HttpCode(HttpStatus.OK)
  async enroll(@Param('id') courseId: string, @CurrentUser() user: User) {
    await this.coursesService.enroll(user.id, courseId);

    return {
      messageFR: 'Inscription réussie',
      messageEN: 'Enrollment successful',
      courseId,
    };
  }
}

5.7 Interceptors pour Logging

// interceptors/logging.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap, catchError } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  private readonly logger = new Logger(LoggingInterceptor.name);

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    const { method, url, body, ip } = request;
    const userAgent = request.get('user-agent') || '';
    const startTime = Date.now();

    this.logger.log(`Incoming Request: ${method} ${url} - IP: ${ip} - UserAgent: ${userAgent}`);

    return next.handle().pipe(
      tap(data => {
        const responseTime = Date.now() - startTime;
        this.logger.log(`Response: ${method} ${url} - ${responseTime}ms`);
      }),
      catchError(error => {
        const responseTime = Date.now() - startTime;
        this.logger.error(`Error Response: ${method} ${url} - ${responseTime}ms`, error.stack);
        throw error;
      })
    );
  }
}

5.8 Main Setup

// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { GlobalExceptionFilter } from './filters/global-exception.filter';
import { ValidationPipe } from './pipes/validation.pipe';
import { LoggingInterceptor } from './interceptors/logging.interceptor';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // Global exception filter
  app.useGlobalFilters(new GlobalExceptionFilter());

  // Global validation pipe
  app.useGlobalPipes(new ValidationPipe());

  // Global logging interceptor
  app.useGlobalInterceptors(new LoggingInterceptor());

  // CORS
  app.enableCors({
    origin: process.env.FRONTEND_URL,
    credentials: true,
  });

  await app.listen(3000);
}
bootstrap();

6. Analyse du Repo ASF Frontend

6.1 Structure Actuelle

Le repo ASF Frontend possĂšde une architecture de gestion d’erreurs bien pensĂ©e avec les composants suivants :

src/
├── core/
│   └── http/
│       └── httpClient.ts          # Client HTTP avec interceptors
├── lib/
│   ├── api/
│   │   └── responseMessages.ts    # Extraction messages d'erreur
│   ├── errors/
│   │   ├── errorTypes.ts          # Types et enums
│   │   ├── errorClassifier.ts     # Classification des erreurs
│   │   └── errorLogger.ts         # Logging centralisĂ©
│   └── toast/
│       └── toastService.ts        # Notifications utilisateur
├── services/
│   ├── courses/
│   │   └── coursesService.ts      # Service des cours
│   └── modules/
│       └── modulesService.ts      # Service des modules
├── components/
│   └── errors/
│       └── ErrorBoundary.tsx      # Error boundary React
└── app/
    ├── global-error.tsx           # Global error handler
    └── [locale]/
        ├── error.tsx              # Error handler locale
        └── dashboard/
            └── error.tsx          # Error handler dashboard

6.2 Points Forts IdentifiĂ©s

✅ 1. SystĂšme de Classification Robuste

// src/lib/errors/errorTypes.ts
enum ErrorCategory {
  NETWORK,
  AUTH,
  VALIDATION,
  NOT_FOUND,
  SERVER,
  UNKNOWN,
  RUNTIME,
}

enum ErrorSeverity {
  LOW,
  MEDIUM,
  HIGH,
  FATAL,
}

interface AppError {
  message: string;
  status?: number;
  category: ErrorCategory;
  severity: ErrorSeverity;
  details?: unknown;
  stack?: string;
  originalError?: Error;
}

Avantage : Permet un traitement différencié selon le type et la gravité.

✅ 2. Error Logger avec Rate Limiting

// src/lib/errors/errorLogger.ts
export class ErrorLogger {
  private static errorCount = 0;
  private static lastResetTime = Date.now();
  private static readonly MAX_ERRORS_PER_MINUTE = 10;

  static log(error: AppError, context: ErrorContext = {}): void {
    // Rate limiting
    if (this.errorCount >= this.MAX_ERRORS_PER_MINUTE) {
      return;
    }

    // Log to console
    // Log to localStorage
    // TODO: Log to monitoring service (Sentry)
  }
}

Avantage : Prévient le spam de logs tout en capturant les erreurs importantes.

✅ 3. Toast Service Intelligent

// src/lib/toast/toastService.ts
export const toastService = {
  error(options: ToastOptions, error?: unknown) {
    const backendMessage = error
      ? extractErrorMessage(error, options.fallbackDescription)
      : options.fallbackDescription;

    toast.error(options.title, { description: backendMessage });
  },
};

Avantage : Extrait automatiquement les messages localisés du backend.

✅ 4. HTTP Client avec Token Refresh

// src/core/http/httpClient.ts
// Token refresh avec queue des requĂȘtes Ă©chouĂ©es
private isRefreshing = false;
private failedQueue: Array<{
  resolve: (value: unknown) => void;
  reject: (error: unknown) => void;
}> = [];

Avantage : GĂšre Ă©lĂ©gamment l’expiration des tokens sans rĂ©pĂ©ter les requĂȘtes.

✅ 5. Error Boundaries Multi-Niveaux

  • global-error.tsx : Erreurs catastrophiques

  • app/[locale]/error.tsx : Erreurs par locale

  • app/[locale]/dashboard/error.tsx : Erreurs spĂ©cifiques au dashboard

Avantage : Isolation des erreurs et gestion contextuelle.

✅ 6. Extraction des Messages Backend

// src/lib/api/responseMessages.ts
export function extractErrorMessage(
  error: unknown,
  fallback: string = 'Une erreur inattendue est survenue.'
): string {
  if (isAxiosError(error) && error.response?.data) {
    const data = error.response.data;

    // Priorité aux messages FR
    if (typeof data.messageFR === 'string' && data.messageFR) {
      return data.messageFR;
    }

    // Fallback sur messageEN
    if (typeof data.messageEN === 'string' && data.messageEN) {
      return data.messageEN;
    }

    // Autres formats...
  }

  return fallback;
}

Avantage : Supporte la localisation et gÚre plusieurs formats de réponse.


7. ProblĂšmes IdentifiĂ©s et Solutions

🔮 PROBLÈME CRITIQUE 1 : Services Sans Gestion d’Erreurs

Localisation : src/services/courses/coursesService.ts, src/services/modules/modulesService.ts

Description : Les services n’ont aucun try/catch. Les erreurs remontent directement aux hooks sans classification ni contexte.

Code actuel :

async getAllCourses(): Promise<Course[]> {
  const response = await this.get<ApiCourseModel[]>(`/cours`);
  if (!response || !Array.isArray(response)) {
    return [];  // ❌ Masque les vraies erreurs
  }
  return response.map(res => CoursesMapper.mapApiCourseToCourseEntity(res));
}

ProblĂšmes :

  1. Impossible de distinguer une rĂ©ponse vide d’une erreur rĂ©seau

  2. Pas de logging des erreurs au niveau service

  3. Pas de contexte ajouté (méthode, paramÚtres)

  4. Les erreurs ne sont pas classifiĂ©es avant d’atteindre le hook

Solution :

async getAllCourses(): Promise<Course[]> {
  try {
    const response = await this.get<ApiCourseModel[]>(`/cours`);

    // Validation de la réponse
    if (!response || !Array.isArray(response)) {
      throw new Error('Invalid response format from API');
    }

    return response.map(res => CoursesMapper.mapApiCourseToCourseEntity(res));
  } catch (error) {
    // Classifier l'erreur
    const classified = classifyError(error);

    // Logger avec contexte
    ErrorLogger.log(classified, {
      service: 'CoursesService',
      method: 'getAllCourses',
      timestamp: new Date().toISOString()
    });

    // Re-throw pour que les hooks puissent gérer
    throw classified;
  }
}

async getCourseById(id: string): Promise<Course> {
  try {
    const response = await this.get<ApiCourseModel>(`/cours/${id}`);

    if (!response) {
      throw new NotFoundError(`Course with id ${id} not found`);
    }

    return CoursesMapper.mapApiCourseToCourseEntity(response);
  } catch (error) {
    // Si c'est une 404, lancer une erreur spécifique
    if (error instanceof HttpError && error.status === 404) {
      throw new NotFoundError(`Course ${id} not found`);
    }

    const classified = classifyError(error);
    ErrorLogger.log(classified, {
      service: 'CoursesService',
      method: 'getCourseById',
      courseId: id
    });

    throw classified;
  }
}

Impact :

  • Meilleure traçabilitĂ© des erreurs

  • Messages plus prĂ©cis pour l’utilisateur

  • Facilite le debugging


🔮 PROBLÈME CRITIQUE 2 : Erreurs de Query IgnorĂ©es dans les Components

Localisation : Tous les components utilisant useQuery

Description : Les hooks retournent error mais les components ne l’utilisent jamais. L’utilisateur ne voit rien en cas d’échec.

Code actuel :

// Hook
const { data: course, error: courseError, isLoading } = useQuery({
  queryKey: ['course_by_id', courseId],
  queryFn: async () => coursesService.getCourseById(courseId),
  enabled: !!courseId,
});

// Component
if (isCourseLoading) {
  return <div>Chargement...</div>;
}

// ❌ Pas de gestion de courseError
// Si courseError existe, course sera undefined mais rien n'est affiché

return <CourseDetails course={course} />; // ❌ Crash si course est undefined

ProblĂšmes :

  1. Utilisateur voit un spinner infini ou un écran blanc

  2. Impossible de savoir si le chargement a échoué

  3. Pas de possibilité de retry

  4. Mauvaise expérience utilisateur

Solution :

// Hook avec gestion d'erreurs
export function useCourse(courseId: string) {
  return useQuery({
    queryKey: ['course_by_id', courseId],
    queryFn: async () => coursesService.getCourseById(courseId),
    enabled: !!courseId,
    retry: (failureCount, error) => {
      // Ne pas retry les 404
      if (error instanceof NotFoundError) {
        return false;
      }
      return failureCount < 3;
    },
    retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
    onError: (error) => {
      const classified = classifyError(error);

      // Log l'erreur
      ErrorLogger.log(classified, {
        hook: 'useCourse',
        courseId
      });

      // Toast seulement pour erreurs graves
      if (classified.severity === ErrorSeverity.FATAL) {
        toastService.error({
          title: 'Erreur de chargement',
          fallbackDescription: 'Impossible de charger le cours'
        }, error);
      }
    }
  });
}

// Component avec gestion complĂšte
export function CoursePage({ courseId }: Props) {
  const {
    data: course,
    isLoading,
    error,
    refetch
  } = useCourse(courseId);

  // Loading state
  if (isLoading) {
    return <CourseLoadingSkeleton />;
  }

  // Error state
  if (error) {
    const classified = classifyError(error);

    // 404 : Cours non trouvé
    if (classified.category === ErrorCategory.NOT_FOUND) {
      return (
        <EmptyState
          icon={<Search className="h-12 w-12" />}
          title="Cours introuvable"
          description="Le cours que vous recherchez n'existe pas ou a été supprimé."
          action={
            <Button onClick={() => router.push('/courses')}>
              Voir tous les cours
            </Button>
          }
        />
      );
    }

    // Autres erreurs
    return (
      <ErrorDisplay
        title="Impossible de charger le cours"
        message={classified.message}
        severity={classified.severity}
        onRetry={classified.retryable ? refetch : undefined}
      />
    );
  }

  // Empty state (ne devrait pas arriver)
  if (!course) {
    return <EmptyState title="Aucun cours" />;
  }

  // Success state
  return <CourseDetails course={course} />;
}

Impact :

  • Utilisateur informĂ© en cas d’erreur

  • PossibilitĂ© de retry

  • Meilleure UX


🔮 PROBLÈME CRITIQUE 3 : Pas de Mapping des Erreurs de Validation Backend

Localisation : Tous les formulaires

Description : La fonction extractValidationErrors() existe mais n’est jamais utilisĂ©e. Les erreurs de validation backend ne sont pas mappĂ©es aux champs du formulaire.

Code actuel :

// useContact.ts
catch (err) {
  toastService.error({ title: "Erreur d'envoi" }, err);
  setError("Une erreur est survenue lors de l'envoi du message.");
  // ❌ Les erreurs de champs spĂ©cifiques sont perdues
}

ProblĂšme : Si le backend retourne :

{
  "statusCode": 422,
  "errors": {
    "email": ["Email invalide", "Email déjà utilisé"],
    "password": ["Minimum 8 caractĂšres"]
  }
}

L’utilisateur voit seulement “Une erreur est survenue”, pas les dĂ©tails.

Solution :

// Hook amélioré
export function useLogin() {
  const { setError } = useFormContext(); // React Hook Form

  return useMutation({
    mutationFn: (payload: LoginPayload) => authService.login(payload),
    onError: (error) => {
      // Erreurs de validation
      if (error instanceof HttpError && error.status === 422) {
        const validationErrors = extractValidationErrors(error);

        if (validationErrors) {
          // Mapper chaque erreur au champ correspondant
          Object.entries(validationErrors).forEach(([field, messages]) => {
            setError(field as keyof LoginPayload, {
              type: 'server',
              message: messages[0] // Prendre le premier message
            });
          });

          // Toast général
          toastService.error({
            title: 'Formulaire invalide',
            fallbackDescription: 'Veuillez corriger les erreurs dans le formulaire'
          });

          return; // Ne pas continuer
        }
      }

      // Autres erreurs
      toastService.error({
        title: 'Erreur de connexion',
        fallbackDescription: 'Impossible de se connecter'
      }, error);
    }
  });
}

// Fonction utilitaire réutilisable
export function useBackendValidation<T extends FieldValues>() {
  const { setError } = useFormContext<T>();

  const mapBackendErrors = (error: unknown) => {
    if (error instanceof HttpError && error.status === 422) {
      const validationErrors = extractValidationErrors(error);

      if (validationErrors) {
        Object.entries(validationErrors).forEach(([field, messages]) => {
          setError(field as Path<T>, {
            type: 'server',
            message: messages[0]
          });
        });
        return true;
      }
    }
    return false;
  };

  return { mapBackendErrors };
}

// Utilisation
export function LoginForm() {
  const form = useForm<LoginFormData>({
    resolver: zodResolver(loginSchema)
  });

  const { mapBackendErrors } = useBackendValidation<LoginFormData>();
  const { mutate: login, isLoading } = useLogin();

  const onSubmit = async (data: LoginFormData) => {
    try {
      await login(data);
    } catch (error) {
      // Mapper les erreurs backend aux champs
      if (!mapBackendErrors(error)) {
        // Si ce n'est pas une erreur de validation, toast générique
        toastService.error({
          title: 'Erreur',
          fallbackDescription: 'Une erreur est survenue'
        }, error);
      }
    }
  };

  return (
    <form onSubmit={form.handleSubmit(onSubmit)}>
      <div>
        <input {...form.register('email')} />
        {form.formState.errors.email && (
          <FormErrorMessage>
            {form.formState.errors.email.message}
          </FormErrorMessage>
        )}
      </div>

      <div>
        <input {...form.register('password')} type="password" />
        {form.formState.errors.password && (
          <FormErrorMessage>
            {form.formState.errors.password.message}
          </FormErrorMessage>
        )}
      </div>

      <button type="submit" disabled={isLoading}>
        {isLoading ? 'Connexion...' : 'Se connecter'}
      </button>
    </form>
  );
}

Impact :

  • Erreurs affichĂ©es au bon endroit (sous chaque champ)

  • Meilleure guidance pour l’utilisateur

  • Validation cohĂ©rente frontend/backend


🔮 PROBLÈME CRITIQUE 4 : Monitoring Non IntĂ©grĂ©ïƒ

Localisation : src/lib/errors/errorLogger.ts

Description : Le code est préparé pour Sentry mais commenté. En production, les erreurs ne sont pas envoyées à un service de monitoring.

Code actuel :

// TODO: Send to monitoring service (Sentry)
if (process.env.NODE_ENV === 'production') {
  // Sentry.captureException(error.originalError || new Error(error.message), {
  //   extra: context
  // });
}

ProblĂšmes :

  1. Impossible de détecter les erreurs en production

  2. Pas de métriques ni de tendances

  3. Debugging difficile sans logs centralisés

  4. Pas d’alertes automatiques

Solution :

// 1. Installer Sentry
// npm install @sentry/nextjs

// 2. Configurer Sentry
// sentry.client.config.ts
import * as Sentry from '@sentry/nextjs';

Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
  environment: process.env.NODE_ENV,
  tracesSampleRate: 1.0,

  // Ignorer certaines erreurs
  ignoreErrors: ['Network request failed', 'ResizeObserver loop limit exceeded'],

  // Filtrer les données sensibles
  beforeSend(event, hint) {
    // Supprimer les tokens
    if (event.request?.headers) {
      delete event.request.headers['Authorization'];
    }
    return event;
  },
});

// 3. Mettre Ă  jour ErrorLogger
export class ErrorLogger {
  static log(error: AppError, context: ErrorContext = {}): void {
    // Rate limiting...

    // Console
    this.logToConsole(error, context);

    // LocalStorage
    this.logToStorage(error, context);

    // Monitoring
    this.logToMonitoring(error, context);
  }

  private static logToMonitoring(error: AppError, context: ErrorContext): void {
    if (process.env.NODE_ENV !== 'production') {
      return;
    }

    // Envoyer Ă  Sentry
    Sentry.captureException(error.originalError || new Error(error.message), {
      level: this.getSentryLevel(error.severity),
      tags: {
        category: error.category,
        errorCode: error.status?.toString(),
      },
      extra: {
        ...context,
        appError: error,
      },
      fingerprint: [error.category, error.message],
    });

    // Métriques personnalisées
    if (error.category === ErrorCategory.AUTH) {
      Sentry.addBreadcrumb({
        category: 'auth',
        message: 'Authentication error occurred',
        level: 'error',
      });
    }
  }

  private static getSentryLevel(severity: ErrorSeverity): Sentry.SeverityLevel {
    switch (severity) {
      case ErrorSeverity.FATAL:
        return 'fatal';
      case ErrorSeverity.HIGH:
        return 'error';
      case ErrorSeverity.MEDIUM:
        return 'warning';
      case ErrorSeverity.LOW:
        return 'info';
    }
  }
}

// 4. Configurer les Error Boundaries avec Sentry
import * as Sentry from '@sentry/nextjs';

export class ErrorBoundary extends Component<Props, State> {
  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    // Log Ă  Sentry avec contexte React
    Sentry.withScope(scope => {
      scope.setContext('react', {
        componentStack: errorInfo.componentStack,
      });
      Sentry.captureException(error);
    });

    // Log local
    const classified = classifyError(error);
    ErrorLogger.log(classified, {
      componentStack: errorInfo.componentStack,
      boundary: 'ErrorBoundary',
    });
  }
}

// 5. Instrumenter les requĂȘtes API
import { sentryTracing } from '@sentry/nextjs';

export class HttpClient {
  async get<T>(url: string): Promise<T> {
    // Créer une transaction Sentry
    const transaction = Sentry.startTransaction({
      name: `GET ${url}`,
      op: 'http.client',
    });

    try {
      const response = await this.client.get<T>(url);
      transaction.setStatus('ok');
      return response.data;
    } catch (error) {
      transaction.setStatus('internal_error');
      throw error;
    } finally {
      transaction.finish();
    }
  }
}

Configuration Sentry recommandée :

// next.config.js
const { withSentryConfig } = require('@sentry/nextjs');

module.exports = withSentryConfig(
  {
    // Next.js config
  },
  {
    // Sentry config
    silent: true,
    org: 'your-org',
    project: 'asf-frontend',
    widenClientFileUpload: true,
    hideSourceMaps: true,
    disableLogger: true,
  }
);

Alertes Ă  configurer :

  1. Taux d’erreur Ă©levĂ© : > 5% des requĂȘtes

  2. Erreurs critiques : Severity FATAL

  3. Nouvelle erreur : Erreur jamais vue avant

  4. RĂ©gression : Augmentation soudaine d’un type d’erreur

Impact :

  • DĂ©tection proactive des problĂšmes

  • MĂ©triques et tendances

  • Debugging facilitĂ© avec stack traces

  • Alertes automatiques


🟠 PROBLÈME MAJEUR 5 : Pas de Retry Logic

Localisation : src/core/http/httpClient.ts, hooks React Query

Description : Aucune stratégie de retry avec exponential backoff pour les erreurs réseau transitoires.

Code actuel :

// React Query avec retry par défaut (1 fois)
useQuery({
  queryKey: ['courses'],
  queryFn: () => coursesService.getAllCourses(),
  // ❌ Retry par dĂ©faut : 1 fois seulement
});

ProblĂšme : Les erreurs rĂ©seau transitoires (timeouts, connexion perdue temporairement) font Ă©chouer les requĂȘtes alors qu’un simple retry rĂ©soudrait le problĂšme.

Solution :

// 1. Configurer React Query globalement
// lib/react-query/queryClient.ts
import { QueryClient } from '@tanstack/react-query';

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: (failureCount, error) => {
        const classified = classifyError(error);

        // Ne jamais retry les erreurs client (4xx)
        if (classified.category === ErrorCategory.VALIDATION ||
            classified.category === ErrorCategory.AUTH ||
            classified.category === ErrorCategory.NOT_FOUND) {
          return false;
        }

        // Retry jusqu'à 3 fois pour les erreurs serveur/réseau
        if (classified.category === ErrorCategory.NETWORK ||
            classified.category === ErrorCategory.SERVER ||
            classified.category === ErrorCategory.TIMEOUT) {
          return failureCount < 3;
        }

        return false;
      },
      retryDelay: (attemptIndex) => {
        // Exponential backoff: 1s, 2s, 4s
        // Avec jitter pour éviter les thundering herds
        const baseDelay = Math.min(1000 * 2 ** attemptIndex, 30000);
        const jitter = Math.random() * 1000;
        return baseDelay + jitter;
      },
      staleTime: 5 * 60 * 1000, // 5 minutes
      cacheTime: 10 * 60 * 1000, // 10 minutes
    },
    mutations: {
      // Pas de retry pour les mutations par défaut
      retry: false
    }
  }
});

// 2. Retry manuel pour les mutations critiques
export function useEnrollCourse() {
  const [retryCount, setRetryCount] = useState(0);
  const maxRetries = 3;

  const mutation = useMutation({
    mutationFn: async (courseId: string) => {
      return await coursesService.enrollCourse(courseId);
    },
    onError: async (error, variables) => {
      const classified = classifyError(error);

      // Retry automatique pour erreurs réseau
      if (classified.category === ErrorCategory.NETWORK &&
          retryCount < maxRetries) {
        const delay = 1000 * 2 ** retryCount;
        await sleep(delay);
        setRetryCount(prev => prev + 1);
        mutation.mutate(variables); // Retry
      } else {
        // Échec dĂ©finitif
        toastService.error({
          title: 'Erreur d\'inscription',
          fallbackDescription: 'Impossible de vous inscrire'
        }, error);
        setRetryCount(0);
      }
    },
    onSuccess: () => {
      setRetryCount(0);
      toastService.success({
        title: 'Inscription réussie'
      });
    }
  });

  return mutation;
}

// 3. Wrapper de retry générique
export async function retryWithBackoff<T>(
  fn: () => Promise<T>,
  options: {
    maxRetries?: number;
    baseDelay?: number;
    shouldRetry?: (error: unknown) => boolean;
    onRetry?: (attempt: number, error: unknown) => void;
  } = {}
): Promise<T> {
  const {
    maxRetries = 3,
    baseDelay = 1000,
    shouldRetry = (error) => {
      const classified = classifyError(error);
      return classified.category === ErrorCategory.NETWORK ||
             classified.category === ErrorCategory.SERVER ||
             classified.category === ErrorCategory.TIMEOUT;
    },
    onRetry
  } = options;

  let lastError: unknown;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error;

      // Vérifier si on doit retry
      if (attempt < maxRetries && shouldRetry(error)) {
        const delay = baseDelay * Math.pow(2, attempt);
        const jitter = Math.random() * 1000;

        onRetry?.(attempt + 1, error);

        await sleep(delay + jitter);
      } else {
        break;
      }
    }
  }

  throw lastError;
}

// Utilisation dans un service
async getAllCourses(): Promise<Course[]> {
  return await retryWithBackoff(
    async () => {
      const response = await this.get<ApiCourseModel[]>('/cours');
      if (!response || !Array.isArray(response)) {
        throw new Error('Invalid response format');
      }
      return response.map(course => CoursesMapper.mapApiCourseToCourseEntity(course));
    },
    {
      maxRetries: 3,
      baseDelay: 1000,
      onRetry: (attempt, error) => {
        console.log(`Retry attempt ${attempt} for getAllCourses`, error);
      }
    }
  );
}

Impact :

  • Meilleure rĂ©silience aux erreurs transitoires

  • Moins d’échecs pour l’utilisateur

  • ExpĂ©rience plus fluide


🟠 PROBLÈME MAJEUR 6 : Timeouts Non ConfigurĂ©s

Localisation : src/core/http/httpClient.ts

Description : Pas de timeout configurĂ© pour les requĂȘtes HTTP. Les requĂȘtes peuvent pendre indĂ©finiment.

Solution :

export class HttpClient {
  private client: AxiosInstance;

  constructor(baseURL: string) {
    this.client = axios.create({
      baseURL,
      timeout: 30000, // 30 secondes
      headers: {
        'Content-Type': 'application/json',
      },
    });

    this.setupInterceptors();
  }

  // Timeouts spécifiques par endpoint
  async uploadFile(file: File): Promise<UploadResponse> {
    // Upload peut prendre plus de temps
    return await this.client.post('/upload', file, {
      timeout: 120000, // 2 minutes
    });
  }

  async search(query: string): Promise<SearchResults> {
    // Search doit ĂȘtre rapide
    return await this.client.get('/search', {
      params: { q: query },
      timeout: 5000, // 5 secondes
    });
  }
}

Impact :

  • PrĂ©vient les requĂȘtes infinies

  • Meilleure UX (Ă©chec rapide plutĂŽt qu’attente)


🟡 PROBLÈME MINEUR 7 : DĂ©tection d’Endpoints Auth Fragile

Localisation : src/core/http/httpClient.ts

Code actuel :

private isAuthEndpoint(url: string): boolean {
  return url.includes('/auth/') || url.endsWith('/login') || url.endsWith('/register');
}

ProblÚme : Utilise des regex/string matching qui peuvent avoir des faux positifs/négatifs.

Solution :

// Liste exhaustive des endpoints auth
private readonly AUTH_ENDPOINTS = new Set([
  '/auth/login',
  '/auth/register',
  '/auth/refresh',
  '/auth/logout',
  '/auth/reset-password',
  '/auth/verify-email'
]);

private isAuthEndpoint(url: string): boolean {
  // Normaliser l'URL
  const normalizedUrl = url.startsWith('/') ? url : `/${url}`;

  // Vérifier correspondance exacte
  return this.AUTH_ENDPOINTS.has(normalizedUrl);
}

// Ou utiliser un pattern plus robuste
private isAuthEndpoint(url: string): boolean {
  const authPattern = /^\/auth\/(login|register|refresh|logout|reset-password|verify-email)$/;
  return authPattern.test(url);
}

Impact :

  • Évite les bugs lors du refresh de token

  • Plus maintenable


🟡 PROBLÈME MINEUR 8 : Erreurs LocalStorage IgnorĂ©es

Localisation : src/lib/errors/errorLogger.ts

Code actuel :

private static logToStorage(error: AppError, context: ErrorContext): void {
  try {
    const logs = JSON.parse(localStorage.getItem(this.STORAGE_KEY) || '[]');
    // ...
    localStorage.setItem(this.STORAGE_KEY, JSON.stringify(logs));
  } catch (e) {
    // ❌ Erreur ignorĂ©e silencieusement
  }
}

Problùme : Si localStorage est plein ou inaccessible, l’erreur est perdue.

Solution :

private static logToStorage(error: AppError, context: ErrorContext): void {
  try {
    const logs = JSON.parse(localStorage.getItem(this.STORAGE_KEY) || '[]');
    logs.push({ error, context, timestamp: new Date().toISOString() });

    // Garder seulement les 50 derniers
    if (logs.length > 50) {
      logs.shift();
    }

    localStorage.setItem(this.STORAGE_KEY, JSON.stringify(logs));
  } catch (storageError) {
    // Fallback : essayer de libérer de l'espace
    try {
      localStorage.clear();
      localStorage.setItem(this.STORAGE_KEY, JSON.stringify([{ error, context }]));
    } catch (finalError) {
      // Dernier recours : log en console seulement
      console.warn('Failed to log to localStorage:', finalError);
      console.error('Original error:', error);
    }
  }
}

Impact :

  • Logs plus fiables

  • Gestion des cas limites


8. Bonnes Pratiques et Recommandations

8.1 Checklist de Gestion d’Erreurs

Pour chaque Couche :

✅ Service Layer

  • [ ] Try/catch autour des appels API

  • [ ] Classification des erreurs

  • [ ] Logging avec contexte

  • [ ] Re-throw pour les couches supĂ©rieures

  • [ ] Validation des rĂ©ponses

✅ Hook Layer

  • [ ] Configuration de retry appropriĂ©e

  • [ ] Gestion des erreurs dans onError

  • [ ] Toast pour erreurs critiques

  • [ ] Invalidation de cache si nĂ©cessaire

✅ Component Layer

  • [ ] Affichage des Ă©tats de loading

  • [ ] Affichage des erreurs

  • [ ] Bouton de retry si applicable

  • [ ] Empty states

✅ Global

  • [ ] Error boundaries Ă  tous les niveaux

  • [ ] Monitoring intĂ©grĂ© (Sentry)

  • [ ] Rate limiting des logs

  • [ ] Messages localisĂ©s

8.2 Patterns RecommandĂ©s

Pattern 1 : Error-First Callback

async function processData(
  data: unknown,
  onSuccess: (result: Result) => void,
  onError: (error: AppError) => void
) {
  try {
    const result = await doSomething(data);
    onSuccess(result);
  } catch (error) {
    const classified = classifyError(error);
    ErrorLogger.log(classified);
    onError(classified);
  }
}

Pattern 2 : Result Type

type Result<T, E = Error> = { success: true; data: T } | { success: false; error: E };

async function getCourse(id: string): Promise<Result<Course>> {
  try {
    const course = await coursesService.getCourseById(id);
    return { success: true, data: course };
  } catch (error) {
    return { success: false, error: classifyError(error) };
  }
}

// Utilisation
const result = await getCourse('123');
if (result.success) {
  console.log(result.data);
} else {
  console.error(result.error);
}

Pattern 3 : Custom Hooks pour Error Handling

// useErrorHandler.ts
export function useErrorHandler() {
  const handleError = useCallback((error: unknown, context?: string) => {
    const classified = classifyError(error);

    ErrorLogger.log(classified, { context });

    // Toast selon sévérité
    if (classified.severity >= ErrorSeverity.MEDIUM) {
      toastService.error({
        title: 'Erreur',
        fallbackDescription: classified.message,
      });
    }

    // Redirection si auth
    if (classified.category === ErrorCategory.AUTH) {
      router.push('/auth/login');
    }
  }, []);

  return { handleError };
}

// Utilisation
const { handleError } = useErrorHandler();

try {
  await someOperation();
} catch (error) {
  handleError(error, 'someOperation');
}

8.3 Messages d’Erreur Efficaces

❌ Mauvais Messages

"Error"
"Something went wrong"
"Error 500"
"Exception occurred"
"Undefined"

✅ Bons Messages

"Impossible de se connecter au serveur. Vérifiez votre connexion internet."
"L'email que vous avez entré est déjà utilisé. Connectez-vous ou utilisez un autre email."
"Le cours est complet. Vous pouvez rejoindre la liste d'attente."
"Votre session a expiré. Veuillez vous reconnecter."

RĂšgles :

  1. Soyez spĂ©cifique : Qu’est-ce qui s’est mal passĂ© ?

  2. Soyez actionnable : Que peut faire l’utilisateur ?

  3. Soyez humain : Langage simple et empathique

  4. Évitez le jargon : Pas de codes ou termes techniques

8.4 Testing de la Gestion d’Erreurs

// __tests__/errorHandling.test.ts
describe('Error Handling', () => {
  it('should display error message when API fails', async () => {
    // Mock l'API pour qu'elle échoue
    jest.spyOn(coursesService, 'getAllCourses').mockRejectedValue(
      new HttpError('Network error', 0)
    );

    // Render le component
    render(<CourseList />);

    // Attendre que l'erreur s'affiche
    await waitFor(() => {
      expect(screen.getByText(/impossible de charger/i)).toBeInTheDocument();
    });

    // Vérifier le bouton retry
    expect(screen.getByText(/réessayer/i)).toBeInTheDocument();
  });

  it('should retry on network error', async () => {
    const mockFn = jest.fn()
      .mockRejectedValueOnce(new Error('Network error'))
      .mockResolvedValueOnce([course1, course2]);

    jest.spyOn(coursesService, 'getAllCourses').mockImplementation(mockFn);

    render(<CourseList />);

    // Attendre le retry automatique
    await waitFor(() => {
      expect(mockFn).toHaveBeenCalledTimes(2);
    });

    // Vérifier que les cours s'affichent
    expect(screen.getByText(course1.title)).toBeInTheDocument();
  });

  it('should map backend validation errors to form fields', async () => {
    const validationError = new HttpError('Validation failed', 422, {
      errors: {
        email: ['Email invalide'],
        password: ['Trop court']
      }
    });

    jest.spyOn(authService, 'login').mockRejectedValue(validationError);

    const { user } = render(<LoginForm />);

    // Remplir et soumettre
    await user.type(screen.getByLabelText(/email/i), 'bad-email');
    await user.type(screen.getByLabelText(/password/i), '123');
    await user.click(screen.getByRole('button', { name: /connexion/i }));

    // Vérifier que les erreurs sont affichées
    await waitFor(() => {
      expect(screen.getByText('Email invalide')).toBeInTheDocument();
      expect(screen.getByText('Trop court')).toBeInTheDocument();
    });
  });
});

9. Exemples Pratiques et Patterns

9.1 Exemple Complet : Page de Cours

// pages/cours/[id].tsx
export default function CoursePage({ params }: { params: { id: string } }) {
  const {
    data: course,
    isLoading,
    error,
    refetch
  } = useCourse(params.id);

  const { mutate: enroll, isLoading: isEnrolling } = useEnrollCourse();

  const handleEnroll = () => {
    enroll(params.id);
  };

  // Loading
  if (isLoading) {
    return (
      <div className="container mx-auto py-8">
        <CourseDetailsSkeleton />
      </div>
    );
  }

  // Error
  if (error) {
    const classified = classifyError(error);

    if (classified.category === ErrorCategory.NOT_FOUND) {
      return (
        <div className="container mx-auto py-16">
          <EmptyState
            icon={<BookOpen className="h-16 w-16 text-gray-400" />}
            title="Cours introuvable"
            description="Le cours que vous recherchez n'existe pas ou a été supprimé."
            action={
              <Link href="/cours">
                <Button>Voir tous les cours</Button>
              </Link>
            }
          />
        </div>
      );
    }

    return (
      <div className="container mx-auto py-16">
        <ErrorDisplay
          title="Impossible de charger le cours"
          message={classified.message}
          severity={classified.severity}
          onRetry={refetch}
        />
      </div>
    );
  }

  // Success
  if (!course) {
    return null;
  }

  return (
    <div className="container mx-auto py-8">
      <div className="grid grid-cols-3 gap-8">
        <div className="col-span-2">
          <CourseHeader course={course} />
          <CourseContent course={course} />
        </div>

        <div className="col-span-1">
          <CourseSidebar
            course={course}
            onEnroll={handleEnroll}
            isEnrolling={isEnrolling}
          />
        </div>
      </div>
    </div>
  );
}

9.2 Exemple : Formulaire avec Validation ComplĂšte

// components/forms/ContactForm.tsx
const contactSchema = z.object({
  name: z.string().min(2, 'Minimum 2 caractĂšres'),
  email: z.string().email('Email invalide'),
  subject: z.string().min(5, 'Minimum 5 caractĂšres'),
  message: z.string().min(10, 'Minimum 10 caractĂšres')
});

type ContactFormData = z.infer<typeof contactSchema>;

export function ContactForm() {
  const form = useForm<ContactFormData>({
    resolver: zodResolver(contactSchema),
    defaultValues: {
      name: '',
      email: '',
      subject: '',
      message: ''
    }
  });

  const { mapBackendErrors } = useBackendValidation<ContactFormData>();
  const { mutate: submit, isLoading } = useSubmitContact();

  const onSubmit = async (data: ContactFormData) => {
    try {
      await submit(data);

      // Reset form on success
      form.reset();

      toastService.success({
        title: 'Message envoyé',
        description: 'Nous vous répondrons dans les plus brefs délais'
      });
    } catch (error) {
      // Mapper erreurs backend
      if (!mapBackendErrors(error)) {
        // Erreur non-validation
        toastService.error({
          title: 'Erreur d\'envoi',
          fallbackDescription: 'Impossible d\'envoyer votre message'
        }, error);
      }
    }
  };

  return (
    <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
      <div>
        <label htmlFor="name" className="block text-sm font-medium">
          Nom *
        </label>
        <input
          id="name"
          {...form.register('name')}
          className="mt-1 block w-full rounded-md border-gray-300"
          aria-invalid={!!form.formState.errors.name}
          aria-describedby={form.formState.errors.name ? 'name-error' : undefined}
        />
        {form.formState.errors.name && (
          <FormErrorMessage id="name-error">
            {form.formState.errors.name.message}
          </FormErrorMessage>
        )}
      </div>

      <div>
        <label htmlFor="email" className="block text-sm font-medium">
          Email *
        </label>
        <input
          id="email"
          type="email"
          {...form.register('email')}
          className="mt-1 block w-full rounded-md border-gray-300"
          aria-invalid={!!form.formState.errors.email}
          aria-describedby={form.formState.errors.email ? 'email-error' : undefined}
        />
        {form.formState.errors.email && (
          <FormErrorMessage id="email-error">
            {form.formState.errors.email.message}
          </FormErrorMessage>
        )}
      </div>

      <div>
        <label htmlFor="subject" className="block text-sm font-medium">
          Sujet *
        </label>
        <input
          id="subject"
          {...form.register('subject')}
          className="mt-1 block w-full rounded-md border-gray-300"
          aria-invalid={!!form.formState.errors.subject}
          aria-describedby={form.formState.errors.subject ? 'subject-error' : undefined}
        />
        {form.formState.errors.subject && (
          <FormErrorMessage id="subject-error">
            {form.formState.errors.subject.message}
          </FormErrorMessage>
        )}
      </div>

      <div>
        <label htmlFor="message" className="block text-sm font-medium">
          Message *
        </label>
        <textarea
          id="message"
          rows={5}
          {...form.register('message')}
          className="mt-1 block w-full rounded-md border-gray-300"
          aria-invalid={!!form.formState.errors.message}
          aria-describedby={form.formState.errors.message ? 'message-error' : undefined}
        />
        {form.formState.errors.message && (
          <FormErrorMessage id="message-error">
            {form.formState.errors.message.message}
          </FormErrorMessage>
        )}
      </div>

      <button
        type="submit"
        disabled={isLoading || !form.formState.isValid}
        className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50"
      >
        {isLoading ? (
          <span className="flex items-center justify-center">
            <Loader className="animate-spin h-5 w-5 mr-2" />
            Envoi en cours...
          </span>
        ) : (
          'Envoyer'
        )}
      </button>
    </form>
  );
}

9.3 Exemple : Gestion d’Erreurs Async avec Promise.allSettled

// hooks/useDashboardData.ts
export function useDashboardData() {
  const [state, setState] = useState<{
    user: User | null;
    courses: Course[];
    statistics: Statistics | null;
    loading: boolean;
    errors: Record<string, AppError | null>;
  }>({
    user: null,
    courses: [],
    statistics: null,
    loading: true,
    errors: {
      user: null,
      courses: null,
      statistics: null
    }
  });

  useEffect(() => {
    loadDashboardData();
  }, []);

  const loadDashboardData = async () => {
    setState(prev => ({ ...prev, loading: true }));

    // Charger toutes les données en parallÚle
    const [userResult, coursesResult, statsResult] = await Promise.allSettled([
      userService.getCurrentUser(),
      coursesService.getEnrolledCourses(),
      statisticsService.getUserStatistics()
    ]);

    // Traiter les résultats
    const newState = {
      loading: false,
      user: userResult.status === 'fulfilled' ? userResult.value : null,
      courses: coursesResult.status === 'fulfilled' ? coursesResult.value : [],
      statistics: statsResult.status === 'fulfilled' ? statsResult.value : null,
      errors: {
        user: userResult.status === 'rejected'
          ? classifyError(userResult.reason)
          : null,
        courses: coursesResult.status === 'rejected'
          ? classifyError(coursesResult.reason)
          : null,
        statistics: statsResult.status === 'rejected'
          ? classifyError(statsResult.reason)
          : null
      }
    };

    setState(newState);

    // Log les erreurs
    Object.entries(newState.errors).forEach(([key, error]) => {
      if (error) {
        ErrorLogger.log(error, { component: 'Dashboard', data: key });
      }
    });

    // Si l'utilisateur n'a pas pu ĂȘtre chargĂ©, c'est critique
    if (userResult.status === 'rejected') {
      toastService.error({
        title: 'Erreur de chargement',
        fallbackDescription: 'Impossible de charger vos informations'
      });
    }
  };

  return { ...state, refetch: loadDashboardData };
}

// Utilisation
export function Dashboard() {
  const { user, courses, statistics, loading, errors, refetch } = useDashboardData();

  if (loading) {
    return <DashboardSkeleton />;
  }

  // Erreur critique : user non chargé
  if (errors.user) {
    return (
      <ErrorDisplay
        title="Impossible de charger le dashboard"
        message={errors.user.message}
        severity={errors.user.severity}
        onRetry={refetch}
      />
    );
  }

  return (
    <div className="container mx-auto py-8">
      <h1>Bienvenue {user?.name}</h1>

      {/* Section cours */}
      {errors.courses ? (
        <ErrorBanner
          message="Impossible de charger vos cours"
          onRetry={refetch}
        />
      ) : (
        <CoursesList courses={courses} />
      )}

      {/* Section statistiques (optionnelle) */}
      {errors.statistics ? (
        <div className="text-sm text-gray-500">
          Statistiques temporairement indisponibles
        </div>
      ) : statistics ? (
        <StatisticsPanel stats={statistics} />
      ) : null}
    </div>
  );
}

Conclusion

La gestion des erreurs est un aspect fondamental de l’ingĂ©nierie logicielle moderne. Une application robuste doit :

  1. Détecter les erreurs rapidement et précisément

  2. Classifier les erreurs pour un traitement approprié

  3. Notifier les utilisateurs avec des messages clairs

  4. Logger les erreurs pour analyse et debugging

  5. RĂ©cupĂ©rer gracieusement quand c’est possible

  6. Monitorer les erreurs en production

Le repo ASF Frontend possĂšde d’excellentes fondations avec son systĂšme de classification, son error logger, et ses error boundaries. Cependant, il reste des amĂ©liorations critiques Ă  apporter :

  • ✅ Ajouter la gestion d’erreurs dans les services

  • ✅ Afficher les erreurs de query dans les components

  • ✅ Mapper les erreurs de validation backend aux formulaires

  • ✅ IntĂ©grer un service de monitoring (Sentry)

  • ✅ ImplĂ©menter une stratĂ©gie de retry

  • ✅ Configurer des timeouts appropriĂ©s

En appliquant ces principes et en implémentant les solutions proposées, vous créerez des applications plus fiables, plus maintenables, et offrant une meilleure expérience utilisateur.


Document créé pour la Masterclass sur la Gestion des Erreurs Date : 2026-01-29 Repo analysé : ASF Frontend (Next.js/React/TypeScript)