academia/docs/entregables/02-diseno/arquitectura/DI-008-manejo-errores.md

6.3 KiB

DI-008: Estrategia de Manejo de Errores

Proyecto: Sistema de Registro de Estudiantes Fecha: 2026-01-07


1. Clasificación de Errores

Tipo Origen Manejo HTTP Code (equiv)
Validación FluentValidation Payload.errors 400
Dominio Domain Exceptions Payload.errors 422
Not Found Repository Payload.errors 404
Conflicto Concurrencia Payload.errors 409
Sistema Excepciones no manejadas Error GraphQL 500

2. Excepciones de Dominio

// Base
public abstract class DomainException : Exception
{
    public string Code { get; }
    protected DomainException(string code, string message) : base(message)
        => Code = code;
}

// Específicas
public class MaxEnrollmentsExceededException : DomainException
{
    public MaxEnrollmentsExceededException()
        : base("MAX_ENROLLMENTS", "Máximo 3 materias permitidas") { }
}

public class SameProfessorConstraintException : DomainException
{
    public SameProfessorConstraintException(string professorName)
        : base("SAME_PROFESSOR", $"Ya tienes una materia con {professorName}") { }
}

public class DuplicateEmailException : DomainException
{
    public DuplicateEmailException()
        : base("DUPLICATE_EMAIL", "Este email ya está registrado") { }
}

public class StudentNotFoundException : DomainException
{
    public StudentNotFoundException(int id)
        : base("NOT_FOUND", $"Estudiante {id} no encontrado") { }
}

3. Patrón Result

public class Result
{
    public bool IsSuccess { get; }
    public IEnumerable<string> Errors { get; }

    protected Result(bool success, IEnumerable<string>? errors = null)
    {
        IsSuccess = success;
        Errors = errors ?? Array.Empty<string>();
    }

    public static Result Success() => new(true);
    public static Result Failure(params string[] errors) => new(false, errors);
}

public class Result<T> : Result
{
    public T? Value { get; }

    private Result(T value) : base(true) => Value = value;
    private Result(IEnumerable<string> errors) : base(false, errors) { }

    public static Result<T> Success(T value) => new(value);
    public static new Result<T> Failure(params string[] errors) => new(errors);
}

4. Handler con Manejo de Errores

public class EnrollStudentHandler
{
    public async Task<EnrollmentPayload> Handle(EnrollInput input)
    {
        try
        {
            var student = await _studentRepo.GetByIdWithEnrollmentsAsync(input.StudentId);
            if (student is null)
                return new EnrollmentPayload(null, ["Estudiante no encontrado"]);

            var subject = await _subjectRepo.GetByIdAsync(input.SubjectId);
            if (subject is null)
                return new EnrollmentPayload(null, ["Materia no encontrada"]);

            // Validación de dominio
            student.Enroll(subject, _enrollmentPolicy);

            await _unitOfWork.SaveChangesAsync();

            var dto = student.Enrollments.Last().Adapt<EnrollmentDto>();
            return new EnrollmentPayload(dto, null);
        }
        catch (DomainException ex)
        {
            return new EnrollmentPayload(null, [ex.Message]);
        }
    }
}

5. Error Filter GraphQL (HotChocolate)

public class ErrorFilter : IErrorFilter
{
    public IError OnError(IError error)
    {
        return error.Exception switch
        {
            DomainException ex => error
                .WithMessage(ex.Message)
                .WithCode(ex.Code),

            ValidationException ex => error
                .WithMessage("Errores de validación")
                .WithCode("VALIDATION_ERROR")
                .SetExtension("errors", ex.Errors.Select(e => e.ErrorMessage)),

            DbUpdateConcurrencyException => error
                .WithMessage("Los datos fueron modificados por otro usuario")
                .WithCode("CONCURRENCY_ERROR"),

            _ => error
                .WithMessage("Error interno del servidor")
                .WithCode("INTERNAL_ERROR")
        };
    }
}

6. Manejo en Frontend

Service

@Injectable({ providedIn: 'root' })
export class ErrorHandlerService {
  private snackBar = inject(MatSnackBar);

  handleGraphQLErrors(errors: string[] | undefined): void {
    if (errors?.length) {
      this.snackBar.open(errors[0], 'Cerrar', {
        duration: 5000,
        panelClass: 'error-snackbar'
      });
    }
  }

  handleApolloError(error: ApolloError): void {
    const message = error.graphQLErrors[0]?.message ?? 'Error de conexión';
    this.snackBar.open(message, 'Cerrar', { duration: 5000 });
  }
}

Componente

enrollStudent(subjectId: number) {
  this.enrollmentService.enroll({
    studentId: this.studentId(),
    subjectId
  }).subscribe({
    next: ({ data }) => {
      if (data?.enrollStudent.errors?.length) {
        this.errorHandler.handleGraphQLErrors(data.enrollStudent.errors);
      } else {
        this.snackBar.open('Inscripción exitosa', 'OK');
      }
    },
    error: (err) => this.errorHandler.handleApolloError(err)
  });
}

7. Códigos de Error Estándar

Código Mensaje Acción UI
VALIDATION_ERROR Errores de validación Mostrar en campos
MAX_ENROLLMENTS Máximo 3 materias Toast + deshabilitar
SAME_PROFESSOR Ya tienes materia con X Toast + deshabilitar
DUPLICATE_EMAIL Email ya registrado Error en campo
NOT_FOUND Recurso no encontrado Redirect + toast
CONCURRENCY_ERROR Datos modificados Refetch + toast
INTERNAL_ERROR Error del servidor Toast genérico

8. Logging de Errores

public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
{
    private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;

    public async Task<TResponse> Handle(TRequest request, ...)
    {
        try
        {
            return await next();
        }
        catch (Exception ex) when (ex is not DomainException)
        {
            _logger.LogError(ex, "Error procesando {Request}", typeof(TRequest).Name);
            throw;
        }
    }
}

Regla: NO loguear datos sensibles (email, contraseñas).