Skip to content

Error Handling

Structured error handling from exceptions to client responses.

Overview

flowchart TB
    A[Exception Thrown] --> B[GlobalExceptionHandler]
    B --> C{Exception Type?}
    C -->|Validation| D[400 Bad Request]
    C -->|MissingData| E[404 Not Found]
    C -->|Auth| F[401 Unauthorized]
    C -->|Forbidden| G[403 Forbidden]
    C -->|Concurrency| H[409 Conflict]
    C -->|Other| I[500 Internal Error]

    D --> J[Problem Details Response]
    E --> J
    F --> J
    G --> J
    H --> J
    I --> J

Exception Types

Domain Exceptions

Exception HTTP Status When to Use
ValidationException 400 Invalid input
MissingDataException 404 Entity not found
AuthException 401 Not authenticated
ForbiddenException 403 Unauthorized action
ConcurrencyException 409 Optimistic locking conflict
TooManyRequestsException 429 Rate limit exceeded

Database Exceptions

Exception HTTP Status When to Use
DbIndexViolationException 409 Unique constraint
DbForeignKeyViolationException 409 FK constraint
DbCheckConstraintViolationException 409 Check constraint
DbNotNullViolationException 400 Required field

Throwing Exceptions

public async Task<MarkerResponse> Handle(GetMarkerQuery request, ...)
{
    var marker = await context.Markers
        .FirstOrDefaultAsync(m => m.MarkerId == request.MarkerId);

    if (marker is null)
    {
        throw new MissingDataException(ErrorCodes.MarkerNotFound);
    }

    return marker.ToResponse();
}

With field-level errors:

throw new ValidationException(new Dictionary<string, string[]>
{
    ["email"] = [ErrorCodes.EmailAlreadyExists]
});

Error Response Format

All errors return consistent JSON:

{
  "code": "VALIDATION_ERROR",
  "message": "One or more validation errors occurred",
  "errors": {
    "title": ["REQUIRED"],
    "latitude": ["INVALID_RANGE"]
  },
  "correlationId": "abc-123-def"
}
Field Description
code Error code constant
message Human-readable message
errors Field-specific errors (validation)
correlationId Request tracking ID

Error Codes

Defined in ErrorCodes.cs:

public static class ErrorCodes
{
    public const string ValidationError = "VALIDATION_ERROR";
    public const string NotFound = "NOT_FOUND";
    public const string Forbidden = "FORBIDDEN";
    public const string Conflict = "CONFLICT";

    public const string MarkerNotFound = "MARKER_NOT_FOUND";
    public const string EmailAlreadyExists = "EMAIL_ALREADY_EXISTS";
    public const string InvalidCredentials = "INVALID_CREDENTIALS";
}

See Error Codes Reference for complete list.

Global Exception Handler

Exception mapping in middleware:

var (statusCode, detail) = ex switch
{
    ValidationException => (400, ex.Message),
    MissingDataException => (404, ex.Message),
    AuthException => (401, ex.Message),
    ForbiddenException => (403, ex.Message),
    TooManyRequestsException => (429, ex.Message),
    ConcurrencyException => (409, "A conflict occurred. Please retry."),
    DbIndexViolationException => (409, "Duplicate entry."),
    _ => (500, "An unexpected error occurred.")
};

Uses RFC 7807 Problem Details format for responses.

Logging

  • 4xx errors: Logged as warnings (expected)
  • 5xx errors: Logged as errors with stack trace
  • Correlation ID included in all logs
logger.LogWarning(
    "Request failed: {ErrorCode} {CorrelationId}",
    errorCode,
    correlationId);