---
name: error-handling-patterns
description: Global error handling, domain exceptions, và Result pattern. Use for exception middleware, validation errors, Polly resiliency, và health checks.
compatibility: ".NET 10+, Polly, FluentValidation, Microsoft.Extensions.Diagnostics.HealthChecks"
metadata:
  author: Velik Ho
  version: "1.0"
---

# Error Handling Patterns / Mẫu Xử Lý Lỗi

Error handling và resiliency patterns cho GoodGo microservices.

## When to Use This Skill / Khi Nào Sử Dụng

Use this skill when:
- Implementing global exception handling / Triển khai xử lý exception toàn cục
- Creating domain exceptions / Tạo domain exceptions
- Setting up retry policies with Polly / Cài đặt retry policies với Polly
- Implementing Circuit Breaker pattern / Triển khai Circuit Breaker
- Configuring health checks / Cấu hình health checks
- Handling validation errors / Xử lý validation errors

## Core Concepts / Khái Niệm Cốt Lõi

### Exception Hierarchy / Phân Cấp Exception

```
ApplicationException (Base)
├── DomainException          # Business rule violations
│   ├── ValidationException  # Input validation errors
│   ├── NotFoundException    # Resource not found
│   └── ConflictException    # Duplicate resource
├── InfrastructureException  # External service failures
│   ├── DatabaseException    # Database errors
│   └── ExternalServiceException # Third-party API errors
└── AuthorizationException   # Permission errors
```

### HTTP Status Code Mapping / Ánh Xạ Status Code

| Exception Type | HTTP Status | Use Case |
|----------------|-------------|----------|
| `ValidationException` | 400 | Invalid input data |
| `UnauthorizedAccessException` | 401 | Missing/invalid token |
| `ForbiddenException` | 403 | No permission |
| `NotFoundException` | 404 | Resource doesn't exist |
| `ConflictException` | 409 | Duplicate resource |
| `DomainException` | 422 | Business rule violation |
| `Exception` | 500 | Unexpected errors |

### Resiliency Patterns / Mẫu Phục Hồi

| Pattern | Purpose | When to Use |
|---------|---------|-------------|
| **Retry** | Thử lại khi lỗi tạm thời | Network timeouts, transient DB errors |
| **Circuit Breaker** | Ngắt mạch khi service chết | External API calls |
| **Timeout** | Giới hạn thời gian chờ | Long-running operations |
| **Bulkhead** | Cô lập tài nguyên | Prevent cascade failures |

## Key Patterns / Mẫu Chính

### Global Exception Middleware

```csharp
/// <summary>
/// EN: Global exception handler middleware.
/// VI: Middleware xử lý exception toàn cục.
/// </summary>
public class ExceptionHandlerMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ExceptionHandlerMiddleware> _logger;

    public ExceptionHandlerMiddleware(
        RequestDelegate next,
        ILogger<ExceptionHandlerMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            await HandleExceptionAsync(context, ex);
        }
    }

    private async Task HandleExceptionAsync(HttpContext context, Exception ex)
    {
        // EN: Log full exception details
        // VI: Log chi tiết exception đầy đủ
        _logger.LogError(ex,
            "EN: Unhandled exception / VI: Exception không xử lý: {Path}",
            context.Request.Path);

        var (statusCode, errorCode, message) = ex switch
        {
            ValidationException ve => (400, "VALIDATION_ERROR", ve.Message),
            UnauthorizedAccessException => (401, "UNAUTHORIZED", "Authentication required"),
            ForbiddenException => (403, "FORBIDDEN", "Access denied"),
            NotFoundException nf => (404, "NOT_FOUND", nf.Message),
            ConflictException ce => (409, "CONFLICT", ce.Message),
            DomainException de => (422, "DOMAIN_ERROR", de.Message),
            _ => (500, "INTERNAL_ERROR", "An unexpected error occurred")
        };

        context.Response.StatusCode = statusCode;
        context.Response.ContentType = "application/json";

        var response = new ApiResponse<object>
        {
            Success = false,
            Error = message,
            ErrorCode = errorCode
        };

        await context.Response.WriteAsJsonAsync(response);
    }
}

// EN: Register in Program.cs / VI: Đăng ký trong Program.cs
app.UseMiddleware<ExceptionHandlerMiddleware>();
```

### Domain Exception Classes

```csharp
/// <summary>
/// EN: Base domain exception.
/// VI: Domain exception cơ sở.
/// </summary>
public class DomainException : Exception
{
    public string ErrorCode { get; }

    public DomainException(string message, string errorCode = "DOMAIN_ERROR")
        : base(message)
    {
        ErrorCode = errorCode;
    }
}

/// <summary>
/// EN: Resource not found exception.
/// VI: Exception không tìm thấy resource.
/// </summary>
public class NotFoundException : DomainException
{
    public NotFoundException(string resourceType, object resourceId)
        : base($"{resourceType} with ID {resourceId} was not found", "NOT_FOUND")
    { }

    public NotFoundException(string message)
        : base(message, "NOT_FOUND")
    { }
}

/// <summary>
/// EN: Conflict/duplicate resource exception.
/// VI: Exception trùng lặp resource.
/// </summary>
public class ConflictException : DomainException
{
    public ConflictException(string message)
        : base(message, "CONFLICT")
    { }
}

/// <summary>
/// EN: Validation exception with errors.
/// VI: Exception validation với danh sách lỗi.
/// </summary>
public class ValidationException : DomainException
{
    public IReadOnlyDictionary<string, string[]> Errors { get; }

    public ValidationException(string message)
        : base(message, "VALIDATION_ERROR")
    {
        Errors = new Dictionary<string, string[]>();
    }

    public ValidationException(IReadOnlyDictionary<string, string[]> errors)
        : base("One or more validation errors occurred", "VALIDATION_ERROR")
    {
        Errors = errors;
    }
}
```

### Retry with Polly

```csharp
/// <summary>
/// EN: Configure Polly retry policies.
/// VI: Cấu hình Polly retry policies.
/// </summary>

// EN: Register resilient HttpClient / VI: Đăng ký HttpClient có khả năng phục hồi
builder.Services.AddHttpClient<IExternalService, ExternalService>(client =>
{
    client.BaseAddress = new Uri(builder.Configuration["Services:External:BaseUrl"]!);
    client.Timeout = TimeSpan.FromSeconds(30);
})
.AddResilienceHandler("external-api", builder =>
{
    // EN: Retry with exponential backoff
    // VI: Retry với thời gian chờ tăng dần
    builder.AddRetry(new HttpRetryStrategyOptions
    {
        MaxRetryAttempts = 3,
        Delay = TimeSpan.FromSeconds(1),
        BackoffType = DelayBackoffType.Exponential,
        ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
            .HandleResult(r => !r.IsSuccessStatusCode && (int)r.StatusCode >= 500)
            .Handle<HttpRequestException>()
            .Handle<TimeoutRejectedException>(),
        OnRetry = args =>
        {
            Console.WriteLine($"Retry {args.AttemptNumber} after {args.RetryDelay}");
            return ValueTask.CompletedTask;
        }
    });

    // EN: Circuit breaker
    // VI: Circuit breaker
    builder.AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions
    {
        FailureRatio = 0.5,
        SamplingDuration = TimeSpan.FromSeconds(30),
        MinimumThroughput = 10,
        BreakDuration = TimeSpan.FromSeconds(30)
    });

    // EN: Timeout
    // VI: Timeout
    builder.AddTimeout(TimeSpan.FromSeconds(10));
});
```

### Health Checks

```csharp
/// <summary>
/// EN: Configure health checks.
/// VI: Cấu hình health checks.
/// </summary>

// EN: Register health checks / VI: Đăng ký health checks
builder.Services.AddHealthChecks()
    .AddNpgSql(
        connectionString: builder.Configuration.GetConnectionString("DefaultConnection")!,
        name: "postgresql",
        tags: new[] { "db", "critical" })
    .AddRedis(
        redisConnectionString: builder.Configuration["Redis:ConnectionString"]!,
        name: "redis",
        tags: new[] { "cache" })
    .AddCheck<ExternalServiceHealthCheck>("external-service", tags: new[] { "external" });

// EN: Map health endpoints / VI: Map health endpoints
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
    Predicate = _ => false // EN: Just check app is running / VI: Chỉ kiểm tra app đang chạy
});

app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
    Predicate = check => check.Tags.Contains("critical")
});

app.MapHealthChecks("/health", new HealthCheckOptions
{
    ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
});
```

### Custom Health Check

```csharp
/// <summary>
/// EN: Custom health check for external service.
/// VI: Health check tùy chỉnh cho external service.
/// </summary>
public class ExternalServiceHealthCheck : IHealthCheck
{
    private readonly HttpClient _httpClient;

    public ExternalServiceHealthCheck(IHttpClientFactory httpClientFactory)
    {
        _httpClient = httpClientFactory.CreateClient("ExternalService");
    }

    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken ct = default)
    {
        try
        {
            var response = await _httpClient.GetAsync("/health", ct);

            return response.IsSuccessStatusCode
                ? HealthCheckResult.Healthy("External service is healthy")
                : HealthCheckResult.Degraded($"External service returned {response.StatusCode}");
        }
        catch (Exception ex)
        {
            return HealthCheckResult.Unhealthy(
                "External service is unavailable",
                exception: ex);
        }
    }
}
```

## Common Mistakes / Lỗi Thường Gặp

### 1. Catching Generic Exceptions

```csharp
// ❌ BAD: Swallowing all exceptions
try
{
    await ProcessOrderAsync(order);
}
catch (Exception)
{
    // Do nothing
}

// ✅ GOOD: Handle specific exceptions
try
{
    await ProcessOrderAsync(order);
}
catch (ValidationException ex)
{
    _logger.LogWarning(ex, "Validation failed");
    throw;
}
catch (Exception ex)
{
    _logger.LogError(ex, "Unexpected error processing order");
    throw;
}
```

### 2. Exposing Internal Errors

```csharp
// ❌ BAD: Exposing stack trace
return StatusCode(500, new { Error = ex.ToString() });

// ✅ GOOD: Generic message for internal errors
return StatusCode(500, new ApiResponse<object>
{
    Success = false,
    Error = "An unexpected error occurred",
    ErrorCode = "INTERNAL_ERROR"
});
```

### 3. Missing Retry Configuration

```csharp
// ❌ BAD: No retry for transient failures
var response = await _httpClient.GetAsync("/api/data");

// ✅ GOOD: Use resilient HttpClient with Polly
// (configured at DI registration level)
var response = await _resilientHttpClient.GetAsync("/api/data");
```

### 4. Ignoring Cancellation Token

```csharp
// ❌ BAD: Ignoring cancellation
await Task.Delay(5000);

// ✅ GOOD: Respect cancellation
await Task.Delay(5000, cancellationToken);
```

## Quick Reference / Tham Chiếu Nhanh

### Error Response Format

```json
{
  "success": false,
  "error": "User-friendly error message",
  "errorCode": "VALIDATION_ERROR",
  "errors": {
    "email": ["Email is required", "Invalid email format"],
    "password": ["Password must be at least 8 characters"]
  }
}
```

### Polly Strategies

| Strategy | Purpose | Typical Config |
|----------|---------|----------------|
| **Retry** | Retry transient failures | 3 attempts, exponential backoff |
| **Circuit Breaker** | Stop calling failing service | 50% failure, 30s break |
| **Timeout** | Limit wait time | 10-30 seconds |
| **Rate Limiter** | Throttle requests | X requests/minute |

### Health Check Tags

| Tag | Purpose |
|-----|---------|
| `critical` | Required for app to function |
| `db` | Database connectivity |
| `cache` | Cache connectivity |
| `external` | External service |

### Common Status Codes

```csharp
return BadRequest(...);    // 400
return Unauthorized(...);  // 401
return Forbid(...);        // 403
return NotFound(...);      // 404
return Conflict(...);      // 409
return UnprocessableEntity(...); // 422
return StatusCode(500, ...);     // 500
```

## Resources / Tài Nguyên

- [Detailed Examples](./references/REFERENCE.md) - Full code examples
- [API Design](../api-design/SKILL.md) - API error responses
- [Security](../security/SKILL.md) - Authentication errors
- [Testing Patterns](../testing-patterns/SKILL.md) - Testing error handling
- [Project Rules](../project-rules/SKILL.md) - Coding standards
