đ Masterclass : Gestion des Erreurs en IngĂ©nierie Logicielleï
Table des MatiĂšresï
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 :
Debugging facilité
Stack traces claires
Contexte de lâerreur
Logs structurés
Monitoring et alertes
Détection proactive des problÚmes
MĂ©triques dâerreurs
Tendances et patterns
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 :
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" }); }
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 :
Taux dâerreur : Nombre dâerreurs / Total de requĂȘtes
CatĂ©gories dâerreurs : Distribution par type
Pages/endpoints affectĂ©s : OĂč se produisent les erreurs
Tendances : Augmentation ou diminution
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 catastrophiquesapp/[locale]/error.tsx: Erreurs par localeapp/[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 :
Impossible de distinguer une rĂ©ponse vide dâune erreur rĂ©seau
Pas de logging des erreurs au niveau service
Pas de contexte ajouté (méthode, paramÚtres)
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 :
Utilisateur voit un spinner infini ou un écran blanc
Impossible de savoir si le chargement a échoué
Pas de possibilité de retry
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 :
Impossible de détecter les erreurs en production
Pas de métriques ni de tendances
Debugging difficile sans logs centralisés
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 :
Taux dâerreur Ă©levĂ© : > 5% des requĂȘtes
Erreurs critiques : Severity FATAL
Nouvelle erreur : Erreur jamais vue avant
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 :
Soyez spĂ©cifique : Quâest-ce qui sâest mal passĂ© ?
Soyez actionnable : Que peut faire lâutilisateur ?
Soyez humain : Langage simple et empathique
Ă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 :
Détecter les erreurs rapidement et précisément
Classifier les erreurs pour un traitement approprié
Notifier les utilisateurs avec des messages clairs
Logger les erreurs pour analyse et debugging
RĂ©cupĂ©rer gracieusement quand câest possible
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)