agentsclimarketplace

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

Install
npx -y skills add FaysilAlshareef/dotnet-ai-kit --skill error-handling

Assembled 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 DomainException base class
  • REST APIs return RFC 9457 ProblemDetails responses
  • gRPC services throw RpcException with StatusCode and Trailers
  • 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 DomainException that implements IProblemDetailsProvider
  • 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 affected
  • UserId — who triggered the operation
  • EventType — what operation was attempted
  • ErrorCode — machine-readable error identifier

Status Code Mapping

Exception TypeHTTP StatusgRPC StatusCode
NotFoundException404NotFound
ValidationException400InvalidArgument
ConcurrencyException409AlreadyExists
DomainException (other)422InvalidArgument
Unhandled Exception500Internal

Anti-Patterns

Anti-PatternCorrect Approach
Catching generic Exception and swallowingCatch specific types, log, re-throw or convert
String error messages without codesUse structured ErrorCode constants
Throwing RpcException from domain layerThrow DomainException, let interceptor convert
Generic "An error occurred" responsesReturn specific ProblemDetails with error code
Logging without context propertiesInclude aggregateId, userId, eventType in every log

Detect Existing Patterns

  1. Search for existing exception base classes in Domain/Exceptions/
  2. Check for IProblemDetailsProvider implementations
  3. Look for gRPC interceptors in Grpc/Interceptors/
  4. Identify error code constants or enums
  5. Follow the established exception hierarchy for new error types

Keep looking

Skills are one crate of 627,801. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.