<skill_overview> <purpose>Implement robust error handling following modern .NET patterns</purpose> <triggers> <trigger>Handling exceptions in services</trigger> <trigger>Implementing validation logic</trigger> <trigger>Designing API error responses</trigger> <trigger>Choosing Result pattern vs exceptions</trigger> <trigger>Setting up global exception handling</trigger> </triggers> <sources> <source url="https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/exceptions/">Microsoft Exception Handling</source> </sources> </skill_overview> <exceptions_vs_result> <use_exceptions_when> <case>Truly exceptional, unexpected failures</case> <case>System-level errors (I/O, network, hardware)</case> <case>In constructors (can't return Result)</case> <case>Framework boundaries expecting exceptions</case> <case>Programming errors (null, out of range)</case> </use_exceptions_when> <use_result_when> <case>Expected business failures (validation, not found)</case> <case>Multiple possible failure modes</case> <case>Performance-critical hot paths</case> <case>Domain layer business logic</case> <case>Compile-time safety for error handling</case> </use_result_when> <example_comparison> <code> // Exception: Unexpected failure if (connection == null) throw new InvalidOperationException("Connection not initialized"); // Result: Expected business outcome public Result<User> GetUser(int id) { var user = _repository.Find(id); if (user == null) return Result.Fail("User not found"); // Expected case return Result.Ok(user); } </code> </example_comparison> </exceptions_vs_result> <exception_best_practices> <throwing> <rule>Throw for truly exceptional conditions only</rule> <rule>Use specific exception types</rule> <rule>Include context in exception message</rule> <example> <code> // Good: Specific exception with context throw new ArgumentNullException(nameof(email), "Email is required for user creation"); // Good: Use built-in guard methods (.NET 6+) ArgumentNullException.ThrowIfNull(user); ArgumentException.ThrowIfNullOrEmpty(email); // Bad: Generic exception throw new Exception("Error occurred"); </code> </example> </throwing> <catching> <rule>Catch specific exceptions first, general last</rule> <rule>Use exception filters (when clause) for fine-grained control</rule> <rule>Always use "throw;" not "throw ex;" to preserve stack trace</rule> <example> <code> try { await ProcessOrderAsync(order); } catch (ValidationException ex) { _logger.LogWarning(ex, "Validation failed for order {OrderId}", order.Id); return BadRequest(ex.Errors); } catch (NotFoundException ex) { return NotFound(ex.Message); } catch (Exception ex) when (ex is TimeoutException or HttpRequestException) { _logger.LogWarning(ex, "Transient error, will retry"); throw; // Preserve stack trace! } catch (Exception ex) { _logger.LogError(ex, "Unexpected error processing order {OrderId}", order.Id); throw; // Never "throw ex;" } </code> </example> </catching> <custom_exceptions> <template> <code> public class OrderProcessingException : Exception { public string OrderId { get; } public string ErrorCode { get; }
public OrderProcessingException(string orderId, string message, string errorCode)
: base(message)
{
OrderId = orderId;
ErrorCode = errorCode;
}
public OrderProcessingException(string orderId, string message, Exception inner)
: base(message, inner)
{
OrderId = orderId;
}
} </code> </template> </custom_exceptions> </exception_best_practices> <result_pattern> <libraries> <library name="FluentResults">Rich API, multiple errors, chaining</library> <library name="ErrorOr">Clean fluent API, functional style</library> <library name="OneOf">Discriminated unions, exhaustive matching</library> </libraries> <erroror_example> <code> public async Task<ErrorOr<Order>> CreateOrderAsync(CreateOrderCommand cmd) { if (cmd.Items.Count == 0) return Error.Validation("Order must have items");
var customer = await _customerRepo.FindAsync(cmd.CustomerId);
if (customer == null)
return Error.NotFound("Customer not found");
if (!customer.IsActive)
return Error.Validation("Customer account is inactive");
var order = new Order(cmd);
await _orderRepo.AddAsync(order);
return order;
} // Controller usage [HttpPost] public async Task<IActionResult> CreateOrder(CreateOrderDto dto) { var result = await _orderService.CreateOrderAsync(dto.ToCommand());
return result.Match(
order => Ok(new OrderDto(order)),
errors => Problem(errors.First().Description)
);
} </code> </erroror_example> <chaining> <code> public async Task<ErrorOr<OrderResult>> ProcessOrderAsync(int orderId) { return await ValidateOrderAsync(orderId) .Then(order => CalculatePriceAsync(order)) .Then(order => ProcessPaymentAsync(order)) .Then(order => SendConfirmationAsync(order)); } </code> </chaining> </result_pattern> <validation> <fluentvalidation recommended="true"> <description>Powerful, fluent validation with separation of concerns</description> <example> <code> public class CreateUserValidator : AbstractValidator<CreateUserDto> { public CreateUserValidator(IUserRepository repository) { RuleFor(x => x.Email) .NotEmpty().WithMessage("Email is required") .EmailAddress().WithMessage("Invalid email format") .MustAsync(async (email, ct) => !await repository.ExistsAsync(email)) .WithMessage("Email already registered");
RuleFor(x => x.Password)
.NotEmpty()
.MinimumLength(8)
.Matches("[A-Z]").WithMessage("Must contain uppercase")
.Matches("[a-z]").WithMessage("Must contain lowercase")
.Matches("[0-9]").WithMessage("Must contain number");
RuleFor(x => x.Age)
.InclusiveBetween(18, 120);
}
} </code> </example> </fluentvalidation> <data_annotations> <description>Simple validation with attributes</description> <use_for>DTOs, simple input validation</use_for> <example> <code> public class CreateUserDto { [Required] [EmailAddress] public string Email { get; init; }
[Required]
[MinLength(8)]
public string Password { get; init; }
[Range(18, 120)]
public int Age { get; init; }
} </code> </example> </data_annotations> <validation_layers> <layer name="Controller">Input format validation (DataAnnotations)</layer> <layer name="Service">Business rules (FluentValidation)</layer> <layer name="Domain">Invariants (guard clauses, exceptions)</layer> </validation_layers> </validation> <aspnetcore_error_handling> <iexceptionhandler recommended="true"> <description>Global exception handling (.NET 8+)</description> <example> <code> public class GlobalExceptionHandler : IExceptionHandler { private readonly ILogger<GlobalExceptionHandler> _logger;
public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger)
{
_logger = logger;
}
public async ValueTask<bool> TryHandleAsync(
HttpContext httpContext,
Exception exception,
CancellationToken ct)
{
_logger.LogError(exception, "Unhandled exception: {Message}", exception.Message);
var problemDetails = exception switch
{
ValidationException ex => new ProblemDetails
{
Type = "https://example.com/validation-error",
Title = "Validation Error",
Status = 400,
Detail = ex.Message
},
NotFoundException => new ProblemDetails
{
Type = "https://example.com/not-found",
Title = "Not Found",
Status = 404
},
_ => new ProblemDetails
{
Type = "https://example.com/internal-error",
Title = "Internal Server Error",
Status = 500,
Detail = "An unexpected error occurred"
}
};
problemDetails.Extensions["traceId"] = httpContext.TraceIdentifier;
httpContext.Response.StatusCode = problemDetails.Status ?? 500;
await httpContext.Response.WriteAsJsonAsync(problemDetails, ct);
return true;
}
} // Registration builder.Services.AddProblemDetails(); builder.Services.AddExceptionHandler<GlobalExceptionHandler>(); app.UseExceptionHandler(); </code> </example> </iexceptionhandler> <problem_details> <description>RFC 7807 standard error format</description> <format> <code> { "type": "https://example.com/validation-error", "title": "Validation Error", "status": 400, "detail": "Email is required", "instance": "/api/users", "traceId": "00-abc123..." } </code> </format> </problem_details> </aspnetcore_error_handling> <logging_errors> <rules> <rule>Always include exception object: LogError(ex, "message")</rule> <rule>Add context: orderId, userId, operation name</rule> <rule>Use structured logging with templates</rule> <rule>Include correlation ID for tracing</rule> </rules> <example> <code> try { await ProcessOrderAsync(order); } catch (Exception ex) { _logger.LogError(ex, "Failed to process order {OrderId} for customer {CustomerId}. Amount: {Amount}", order.Id, order.CustomerId, order.TotalAmount); throw; } </code> </example> <never_log> <item>Passwords (even hashed)</item> <item>Credit card numbers</item> <item>API keys and tokens</item> <item>Personal identification numbers</item> </never_log> </logging_errors> <anti_patterns> <avoid name="empty_catch"> <bad>catch (Exception) { }</bad> <why>Swallows errors silently</why> </avoid> <avoid name="catch_and_throw_new"> <bad>catch (Exception ex) { throw new Exception("Error", ex); }</bad> <why>Loses original exception type</why> </avoid> <avoid name="throw_ex"> <bad>throw ex;</bad> <why>Resets stack trace</why> <good>throw;</good> </avoid> <avoid name="exception_for_flow_control"> <bad>Using exceptions for expected business logic</bad> <why>Expensive, unclear intent</why> <good>Use Result pattern for expected failures</good> </avoid> </anti_patterns>