# 18. Error Handling ## Unified Error Types ```typescript // src/lib/errors.ts export enum ErrorCode { // Validation (400) VALIDATION_ERROR = 'VALIDATION_ERROR', INVALID_INPUT = 'INVALID_INPUT', // Authentication (401) UNAUTHORIZED = 'UNAUTHORIZED', SESSION_EXPIRED = 'SESSION_EXPIRED', // Authorization (403) FORBIDDEN = 'FORBIDDEN', INSUFFICIENT_PERMISSIONS = 'INSUFFICIENT_PERMISSIONS', // Not Found (404) NOT_FOUND = 'NOT_FOUND', RESOURCE_NOT_FOUND = 'RESOURCE_NOT_FOUND', // Conflict (409) CONFLICT = 'CONFLICT', DUPLICATE_ENTRY = 'DUPLICATE_ENTRY', // External Service (502) EXTERNAL_API_ERROR = 'EXTERNAL_API_ERROR', DOFUSDB_ERROR = 'DOFUSDB_ERROR', // Server (500) INTERNAL_ERROR = 'INTERNAL_ERROR', DATABASE_ERROR = 'DATABASE_ERROR', } export interface AppErrorData { code: ErrorCode; message: string; details?: Record; field?: string; } export class AppError extends Error { public readonly code: ErrorCode; public readonly statusCode: number; public readonly details?: Record; public readonly field?: string; public readonly isOperational: boolean; constructor(data: AppErrorData & { statusCode?: number }) { super(data.message); this.name = 'AppError'; this.code = data.code; this.statusCode = data.statusCode ?? this.getDefaultStatusCode(data.code); this.details = data.details; this.field = data.field; this.isOperational = true; Error.captureStackTrace(this, this.constructor); } private getDefaultStatusCode(code: ErrorCode): number { const statusMap: Record = { [ErrorCode.VALIDATION_ERROR]: 400, [ErrorCode.INVALID_INPUT]: 400, [ErrorCode.UNAUTHORIZED]: 401, [ErrorCode.SESSION_EXPIRED]: 401, [ErrorCode.FORBIDDEN]: 403, [ErrorCode.INSUFFICIENT_PERMISSIONS]: 403, [ErrorCode.NOT_FOUND]: 404, [ErrorCode.RESOURCE_NOT_FOUND]: 404, [ErrorCode.CONFLICT]: 409, [ErrorCode.DUPLICATE_ENTRY]: 409, [ErrorCode.EXTERNAL_API_ERROR]: 502, [ErrorCode.DOFUSDB_ERROR]: 502, [ErrorCode.INTERNAL_ERROR]: 500, [ErrorCode.DATABASE_ERROR]: 500, }; return statusMap[code] ?? 500; } toJSON(): AppErrorData & { statusCode: number } { return { code: this.code, message: this.message, statusCode: this.statusCode, details: this.details, field: this.field, }; } } ``` ## Backend Error Handler ```typescript // src/lib/server/error-handler.ts import { Prisma } from '@prisma/client'; import { AppError, ErrorCode } from '@/lib/errors'; export function handlePrismaError(error: unknown): AppError { if (error instanceof Prisma.PrismaClientKnownRequestError) { switch (error.code) { case 'P2002': return new AppError({ code: ErrorCode.DUPLICATE_ENTRY, message: 'Cette entrée existe déjà', details: { fields: error.meta?.target }, }); case 'P2025': return new AppError({ code: ErrorCode.NOT_FOUND, message: 'Ressource non trouvée', }); default: return new AppError({ code: ErrorCode.DATABASE_ERROR, message: 'Erreur de base de données', }); } } return new AppError({ code: ErrorCode.INTERNAL_ERROR, message: 'Une erreur inattendue est survenue', }); } ``` ## Frontend Error Handler ```typescript // src/lib/client/error-handler.ts import { toast } from 'sonner'; import { AppError, ErrorCode } from '@/lib/errors'; const errorMessages: Record = { [ErrorCode.VALIDATION_ERROR]: 'Données invalides', [ErrorCode.UNAUTHORIZED]: 'Veuillez vous connecter', [ErrorCode.NOT_FOUND]: 'Ressource non trouvée', [ErrorCode.DUPLICATE_ENTRY]: 'Cette entrée existe déjà', [ErrorCode.INTERNAL_ERROR]: 'Erreur serveur', // ... other codes }; export function handleError(error: unknown): void { if (error instanceof AppError) { const message = error.message || errorMessages[error.code]; if (error.statusCode >= 500) { toast.error('Erreur serveur', { description: message }); } else { toast.warning(message); } return; } console.error('Unexpected error:', error); toast.error('Une erreur inattendue est survenue'); } ``` ---