skills/core/error-handling/SKILL.md
Use when implementing error handling with domain exceptions, ProblemDetails, or RpcException mapping.
npx skillsauth add faysilalshareef/dotnet-ai-kit error-handlingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
DomainException base classRpcException with StatusCode and Trailersnamespace {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;
}
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";
}
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;
}
{
"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"
}
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:
DomainException that implements IProblemDetailsProviderproblem-details-bin)StatusCode.Internal// In Program.cs or GrpcContainer
builder.Services.AddGrpc(options =>
{
options.Interceptors.Add<ApplicationExceptionInterceptor>();
});
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| Exception Type | HTTP Status | gRPC StatusCode |
|---|---|---|
| NotFoundException | 404 | NotFound |
| ValidationException | 400 | InvalidArgument |
| ConcurrencyException | 409 | AlreadyExists |
| DomainException (other) | 422 | InvalidArgument |
| Unhandled Exception | 500 | Internal |
| 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 |
Domain/Exceptions/IProblemDetailsProvider implementationsGrpc/Interceptors/data-ai
Use when about to claim work is complete, fixed, passing, or ready — before committing, creating PRs, or moving to the next task. Requires running verification commands and confirming output before making any success claims.
development
Use when encountering any bug, test failure, build error, or unexpected behavior — before proposing fixes or making changes.
development
Use when checkpointing, wrapping up, or handing off an AI-assisted development session.
development
Use when following the Specification-Driven Development lifecycle from plan through ship.