# 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 ```csharp // 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 ```csharp public class Result { public bool IsSuccess { get; } public IEnumerable Errors { get; } protected Result(bool success, IEnumerable? errors = null) { IsSuccess = success; Errors = errors ?? Array.Empty(); } public static Result Success() => new(true); public static Result Failure(params string[] errors) => new(false, errors); } public class Result : Result { public T? Value { get; } private Result(T value) : base(true) => Value = value; private Result(IEnumerable errors) : base(false, errors) { } public static Result Success(T value) => new(value); public static new Result Failure(params string[] errors) => new(errors); } ``` --- ## 4. Handler con Manejo de Errores ```csharp public class EnrollStudentHandler { public async Task 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(); return new EnrollmentPayload(dto, null); } catch (DomainException ex) { return new EnrollmentPayload(null, [ex.Message]); } } } ``` --- ## 5. Error Filter GraphQL (HotChocolate) ```csharp 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 ```typescript @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 ```typescript 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 ```csharp public class LoggingBehavior : IPipelineBehavior { private readonly ILogger> _logger; public async Task 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).