Error handling
Skill FaysilAlshareef/dotnet-ai-kit/skills/core/error-handling
Use when implementing error handling with domain exceptions, ProblemDetails, or RpcException mapping.From its SKILL.md
npx -y skills add FaysilAlshareef/dotnet-ai-kit --skill error-handlingAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 4 stars4 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
7.1 KB, as published. Nobody here has run it
Error Handling Patterns
Core Principles
- Domain exceptions inherit from a
DomainExceptionbase class - REST APIs return RFC 9457 ProblemDetails responses
- gRPC services throw
RpcExceptionwithStatusCodeandTrailers - Interceptors handle automatic exception-to-error mapping
- Structured error codes identify specific failure scenarios
- Never swallow exceptions — log with structured context then re-throw or convert
Exception Hierarchy
namespace {Company}.{Domain}.Domain.Exceptions;
public abstract class DomainException(
string message,
string errorCode,
Exception? innerException = null) : Exception(message, innerException)
{
public string ErrorCode { get; } = errorCode;
}
public class NotFoundException(
string entityName,
object entityId)
: DomainException(
$"{entityName} with ID '{entityId}' was not found.",
$"{entityName.ToUpperInvariant()}_NOT_FOUND")
{
public string EntityName { get; } = entityName;
public object EntityId { get; } = entityId;
}
public class ValidationException(
string message,
string errorCode,
IDictionary<string, string[]>? errors = null)
: DomainException(message, errorCode)
{
public IDictionary<string, string[]> Errors { get; } =
errors ?? new Dictionary<string, string[]>();
}
public class ConcurrencyException(
string entityName,
object entityId)
: DomainException(
$"Concurrency conflict on {entityName} with ID '{entityId}'.",
$"{entityName.ToUpperInvariant()}_CONCURRENCY_CONFLICT")
{
public string EntityName { get; } = entityName;
public object EntityId { get; } = entityId;
}
Structured Error Codes
Use uppercase snake_case constants scoped to the domain:
public static class ErrorCodes
{
public const string DrawNotFound = "DRAW_NOT_FOUND";
public const string InvalidSequence = "INVALID_SEQUENCE";
public const string DuplicateEntry = "DUPLICATE_ENTRY";
public const string InsufficientBalance = "INSUFFICIENT_BALANCE";
public const string OrderAlreadyCancelled = "ORDER_ALREADY_CANCELLED";
}
IProblemDetailsProvider (REST)
using Microsoft.AspNetCore.Http;
namespace {Company}.{Domain}.Domain.Exceptions;
public interface IProblemDetailsProvider
{
string Type { get; }
string Title { get; }
int Status { get; }
string Detail { get; }
}
public class NotFoundException : DomainException, IProblemDetailsProvider
{
// ... constructor and properties as above ...
public string Type => "https://tools.ietf.org/html/rfc9110#section-15.5.5";
public string Title => "Not Found";
public int Status => StatusCodes.Status404NotFound;
public string Detail => Message;
}
ProblemDetails Response Format
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.5",
"title": "Not Found",
"status": 404,
"detail": "Draw with ID '3fa85f64-5717-4562-b3fc-2c963f66afa6' was not found.",
"instance": "/api/draws/3fa85f64-5717-4562-b3fc-2c963f66afa6",
"errorCode": "DRAW_NOT_FOUND"
}
gRPC RpcException Mapping
using Grpc.Core;
using System.Text.Json;
namespace {Company}.{Domain}.Grpc.Interceptors;
public class ApplicationExceptionInterceptor : Interceptor
{
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
TRequest request,
ServerCallContext context,
UnaryServerMethod<TRequest, TResponse> continuation)
{
try
{
return await continuation(request, context);
}
catch (DomainException ex) when (ex is IProblemDetailsProvider provider)
{
var metadata = new Metadata();
var problemDetails = new
{
provider.Type,
provider.Title,
provider.Status,
provider.Detail,
ex.ErrorCode
};
metadata.Add("problem-details-bin",
JsonSerializer.SerializeToUtf8Bytes(problemDetails));
throw new RpcException(
new Status(MapToGrpcStatusCode(provider.Status), ex.Message),
metadata);
}
}
private static StatusCode MapToGrpcStatusCode(int httpStatus) => httpStatus switch
{
400 => StatusCode.InvalidArgument,
404 => StatusCode.NotFound,
409 => StatusCode.AlreadyExists,
422 => StatusCode.InvalidArgument,
_ => StatusCode.Internal
};
}
Key details:
- Catches
DomainExceptionthat implementsIProblemDetailsProvider - Serializes problem details to gRPC binary metadata trailer (
problem-details-bin) - Maps HTTP status codes to gRPC status codes
- Uncaught exceptions become
StatusCode.Internal
gRPC Interceptor Registration
// In Program.cs or GrpcContainer
builder.Services.AddGrpc(options =>
{
options.Interceptors.Add<ApplicationExceptionInterceptor>();
});
Error Logging with Structured Context
public async Task<bool> Handle(
Event<OrderCreatedData> @event,
CancellationToken cancellationToken)
{
try
{
// ... handler logic ...
}
catch (DomainException ex)
{
_logger.LogError(ex,
"Domain error processing event {EventType} for aggregate {AggregateId}. " +
"ErrorCode: {ErrorCode}, UserId: {UserId}",
@event.Type,
@event.AggregateId,
ex.ErrorCode,
@event.UserId);
throw;
}
}
Always include in structured log context:
AggregateId— which entity was affectedUserId— who triggered the operationEventType— what operation was attemptedErrorCode— machine-readable error identifier
Status Code Mapping
| Exception Type | HTTP Status | gRPC StatusCode |
|---|---|---|
NotFoundException | 404 | NotFound |
ValidationException | 400 | InvalidArgument |
ConcurrencyException | 409 | AlreadyExists |
DomainException (other) | 422 | InvalidArgument |
Unhandled Exception | 500 | Internal |
Anti-Patterns
| Anti-Pattern | Correct Approach |
|---|---|
Catching generic Exception and swallowing | Catch specific types, log, re-throw or convert |
| String error messages without codes | Use structured ErrorCode constants |
Throwing RpcException from domain layer | Throw DomainException, let interceptor convert |
| Generic "An error occurred" responses | Return specific ProblemDetails with error code |
| Logging without context properties | Include aggregateId, userId, eventType in every log |
Detect Existing Patterns
- Search for existing exception base classes in
Domain/Exceptions/ - Check for
IProblemDetailsProviderimplementations - Look for gRPC interceptors in
Grpc/Interceptors/ - Identify error code constants or enums
- Follow the established exception hierarchy for new error types