DotNetApiErrors 1.0.0

dotnet add package DotNetApiErrors --version 1.0.0
                    
NuGet\Install-Package DotNetApiErrors -Version 1.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="DotNetApiErrors" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="DotNetApiErrors" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="DotNetApiErrors" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add DotNetApiErrors --version 1.0.0
                    
#r "nuget: DotNetApiErrors, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package DotNetApiErrors@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=DotNetApiErrors&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=DotNetApiErrors&version=1.0.0
                    
Install as a Cake Tool

DotNetApiErrors

Global error handling middleware for consistent API error responses, proper exception logging, and error code management.

Problems Solved

  • Try/catch everywhere: Repetitive error handling code in every controller
  • No global exception handling: Exceptions bubble up as 500 errors without context
  • Inconsistent error responses: Different error formats across endpoints
  • Stack traces leaked: Stack traces exposed to clients in production
  • No error codes: Clients can't programmatically handle specific errors
  • Swallowed exceptions: Exceptions logged but not rethrown, losing context
  • Logging without rethrowing: Errors logged but exception chain broken
  • Retry on non-retriable errors: Retrying errors that will never succeed
  • Exception to HTTP status mapping: Wrong HTTP status codes for exceptions
  • Domain vs system exceptions: No distinction between business and system errors

Installation

dotnet add package DotNetApiErrors

Quick Start

1. Global Exception Handler

Problem: Exceptions return generic 500 errors without context or error codes.

using DotNetApiErrors;

// ✅ GOOD: Add global exception handler
app.UseGlobalExceptionHandler(options =>
{
    // Map domain exceptions to HTTP status codes
    options.MapException<ValidationException>(HttpStatusCode.BadRequest, "VALIDATION_ERROR");
    options.MapException<NotFoundException>(HttpStatusCode.NotFound, "NOT_FOUND");
    options.MapException<UnauthorizedException>(HttpStatusCode.Unauthorized, "UNAUTHORIZED");
    
    // Hide stack traces in production
    options.HideStackTraceInProduction = true;
    options.IncludeDetails = true;
});

2. Consistent Error Response Format

Problem: Different endpoints return errors in different formats.

// ✅ GOOD: All errors return consistent format
{
    "error": {
        "code": "VALIDATION_ERROR",
        "message": "Invalid input provided",
        "details": {
            "field": "email",
            "reason": "Invalid email format"
        }
    }
}

3. Domain Exceptions

Problem: No distinction between business logic errors and system errors.

// ✅ GOOD: Use domain exceptions
public class OrderService
{
    public async Task<Order> GetOrderAsync(int orderId)
    {
        var order = await _repository.FindAsync(orderId);
        if (order == null)
        {
            throw new NotFoundException($"Order {orderId} not found");
        }
        return order;
    }
    
    public async Task CreateOrderAsync(CreateOrderRequest request)
    {
        if (request.Items.Count == 0)
        {
            throw new ValidationException(
                "Order must have at least one item",
                new Dictionary<string, string[]>
                {
                    ["Items"] = new[] { "At least one item is required" }
                }
            );
        }
        
        // Create order...
    }
}

4. Exception Extensions

Problem: Exceptions logged but not rethrown, or retried when they shouldn't be.

// ✅ GOOD: Log and rethrow properly
try
{
    await ProcessOrderAsync();
}
catch (Exception ex)
{
    // Log and rethrow to maintain exception chain
    ex.LogAndRethrow(logger, rethrow: true);
}

// ✅ GOOD: Check if exception is retriable
if (exception.IsRetriable())
{
    await RetryOperationAsync();
}
else
{
    throw; // Don't retry non-retriable errors
}

Real-World Example

// Program.cs
var app = builder.Build();

// Configure global exception handler
app.UseGlobalExceptionHandler(options =>
{
    // Map business exceptions
    options.MapException<ValidationException>(HttpStatusCode.BadRequest, "VALIDATION_ERROR");
    options.MapException<NotFoundException>(HttpStatusCode.NotFound, "NOT_FOUND");
    options.MapException<UnauthorizedException>(HttpStatusCode.Unauthorized, "UNAUTHORIZED");
    options.MapException<PaymentException>(HttpStatusCode.PaymentRequired, "PAYMENT_REQUIRED");
    
    // Production settings
    options.HideStackTraceInProduction = true;
    options.IncludeDetails = true;
});

// Controller
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
    private readonly IOrderService _orderService;
    
    [HttpGet("{id}")]
    public async Task<ActionResult<Order>> GetOrder(int id)
    {
        // Domain exception automatically converted to proper HTTP response
        var order = await _orderService.GetOrderAsync(id);
        return Ok(order);
    }
    
    [HttpPost]
    public async Task<ActionResult<Order>> CreateOrder(CreateOrderRequest request)
    {
        // Validation exception automatically returns 400 with error details
        var order = await _orderService.CreateOrderAsync(request);
        return CreatedAtAction(nameof(GetOrder), new { id = order.Id }, order);
    }
}

// Service
public class OrderService : IOrderService
{
    public async Task<Order> GetOrderAsync(int id)
    {
        var order = await _repository.FindAsync(id);
        if (order == null)
        {
            throw new NotFoundException($"Order {id} not found");
        }
        return order;
    }
    
    public async Task<Order> CreateOrderAsync(CreateOrderRequest request)
    {
        // Validate
        if (string.IsNullOrWhiteSpace(request.CustomerEmail))
        {
            throw new ValidationException(
                "Customer email is required",
                new Dictionary<string, string[]>
                {
                    ["CustomerEmail"] = new[] { "Email is required" }
                }
            );
        }
        
        // Business logic...
        return await _repository.CreateAsync(order);
    }
}

Error Response Examples

Validation Error (400)

{
    "error": {
        "code": "VALIDATION_ERROR",
        "message": "Invalid input provided",
        "details": {
            "Email": ["Invalid email format"],
            "Age": ["Age must be between 18 and 100"]
        }
    }
}

Not Found Error (404)

{
    "error": {
        "code": "NOT_FOUND",
        "message": "Order 123 not found"
    }
}

Unauthorized Error (401)

{
    "error": {
        "code": "UNAUTHORIZED",
        "message": "Unauthorized access"
    }
}

Internal Server Error (500) - Production

{
    "error": {
        "code": "INTERNAL_ERROR",
        "message": "An error occurred while processing your request."
    }
}

Internal Server Error (500) - Development

{
    "error": {
        "code": "INTERNAL_ERROR",
        "message": "Object reference not set to an instance of an object.",
        "details": {
            "type": "NullReferenceException",
            "stackTrace": "..."
        }
    }
}

Best Practices

  1. Use domain exceptions (ValidationException, NotFoundException, etc.) for business logic errors
  2. Map exceptions to HTTP status codes in the global handler
  3. Hide stack traces in production but include them in development
  4. Use error codes for programmatic error handling by clients
  5. Log and rethrow exceptions to maintain exception chain
  6. Check if exceptions are retriable before retrying operations
  7. Use consistent error format across all endpoints

API Reference

  • GlobalExceptionHandlerMiddleware - Middleware for global exception handling
  • DomainException - Base class for domain exceptions
  • ValidationException - Exception for validation errors
  • NotFoundException - Exception for not found errors
  • UnauthorizedException - Exception for unauthorized access
  • ExceptionExtensions - Extension methods for exception handling
Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0 119 12/29/2025

Initial release. See README for details.