Files
dofus-manager/docs/architecture/18-error-handling.md
2026-01-19 08:52:38 +01:00

4.3 KiB

18. Error Handling

Unified Error Types

// 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<string, unknown>;
  field?: string;
}

export class AppError extends Error {
  public readonly code: ErrorCode;
  public readonly statusCode: number;
  public readonly details?: Record<string, unknown>;
  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, number> = {
      [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

// 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

// src/lib/client/error-handler.ts

import { toast } from 'sonner';
import { AppError, ErrorCode } from '@/lib/errors';

const errorMessages: Record<ErrorCode, string> = {
  [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');
}