Error Handling_ in_ Web API

Error Handling in ASP.NET Core Web API - Complete Guide

Table of Contents

  1. Overview
  2. Error Handling Methods
  3. Implementation Examples
  4. When to Use Each Method
  5. Interview Questions (3+ Years)

Overview

Error handling in Web API is crucial for:

  • Providing meaningful error responses to clients
  • Logging errors for debugging and monitoring
  • Maintaining API stability and security
  • Following REST conventions (proper HTTP status codes)
  • Improving user experience with clear error messages

Error Handling Methods

1. Try-Catch Blocks

The most basic and direct approach to handle exceptions.

Implementation:

[HttpGet("{id}")]
public IActionResult GetUser(int id)
{
    try
    {
        if (id <= 0)
            return BadRequest(new { message = "Invalid ID" });

        var user = _userService.GetUserById(id);
        
        if (user == null)
            return NotFound(new { message = "User not found" });

        return Ok(user);
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Error retrieving user");
        return StatusCode(StatusCodes.Status500InternalServerError, 
            new { message = "An error occurred while processing your request" });
    }
}

When to Use:

  • Simple, localized error handling
  • When you need specific control over error logic
  • For business logic validation
  • When handling different types of exceptions differently

Pros:

  • Explicit and clear
  • Direct control over error handling
  • Easy to understand

Cons:

  • Repetitive if used across many methods
  • Violates DRY principle
  • Code becomes cluttered with try-catch blocks

2. Global Exception Middleware

A centralized approach to handle all unhandled exceptions across the application.

Implementation:

Custom Middleware Class:

public class GlobalExceptionMiddleware
{
    // Dependencies injected via constructor
    private readonly RequestDelegate next; // Delegates to next middleware
    private readonly ILogger<ExceptionMiddleware> logger; // Logs errors
    private readonly IHostEnvironment environment; // Detects Dev/Production

    public GlobalExceptionMiddleware(RequestDelegate next, ILogger<ExceptionMiddleware> logger, IHostEnvironment environment)
    {
        this.next = next;
        this.logger = logger;
        this.environment = environment;
    }

    // Core Logic 
    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            // Execute the next middleware in pipeline
            await next(context);
        }
        catch (Exception ex)
        {
            // Log the error with full exception details
                logger.LogError(ex, ex.Message);

                  // Set response format
                context.Response.ContentType = "application/json";
                context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;

                // Create different response based on environment
                var response = environment.IsDevelopment() ? new ApiException(context.Response.StatusCode, ex.Message, ex.StackTrace?.ToString()) // Show stack trace in Development
                    : new ApiException(context.Response.StatusCode, ex.Message, "Internal Server Error"); // Hide details in Production

                // Serialize to JSON with camelCase naming
                var option = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
                var json = JsonSerializer.Serialize(response, option);

                // Send response to client
                await context.Response.WriteAsync(json);
        }
    }
}

 public class ApiException
 {
    // Constructor initializes all properties
     public ApiException(int statusCode, string message, string details)
     {
         StatusCode = statusCode;
         Message = message;
         Details = details;
     }
    // Properties that get serialized to JSON
     public int StatusCode { get; set; }
     public string Message { get; set; }
     public string Details { get; set; }
 }

Register in Program.cs:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddLogging();

var app = builder.Build();

// Register middleware before other middlewares
app.UseMiddleware<GlobalExceptionMiddleware>();

app.UseRouting();
app.MapControllers();

app.Run();

How It Works (Flow)

Request → GlobalExceptionMiddleware
           ↓
        try { await next(context) }
           ↓
        Controller executes
           ↓
     Exception thrown?
      ↙           ↘
    No            Yes
    ↓              ↓
Response sent   Catch exception
                   ↓
            Log error details
                   ↓
            Set Status Code 500
                   ↓
         Create ApiException response
                   ↓
      Check environment (Dev/Prod)
       ↙                      ↘
    Dev               Production
    ↓                      ↓
Include          Hide stack trace
stack trace      (security)
    ↓                      ↓
Serialize to JSON
    ↓
Send to client

When to Use:

  • Catching all unhandled exceptions
  • Consistent error response format across API
  • Centralized logging and error handling
  • Production-level applications

Pros:

  • DRY principle - handle all exceptions in one place
  • Consistent error responses
  • Cleaner controller code
  • Easy to maintain and update

Cons:

  • Less granular control per endpoint
  • May catch exceptions you don't want to handle
  • Harder to debug specific scenarios

3. Exception Filters

Attribute-based filtering to handle exceptions at the action level.

Implementation:

Custom Exception Filter:

public class CustomExceptionFilter : IExceptionFilter
{
    private readonly ILogger<CustomExceptionFilter> _logger;

    public CustomExceptionFilter(ILogger<CustomExceptionFilter> logger)
    {
        _logger = logger;
    }

    public void OnException(ExceptionContext context)
    {
        // This method is called when an exception occurs
        _logger.LogError(context.Exception, "Exception occurred");

        var response = new ErrorResponse
        {
            Message = "An error occurred",
            TraceId = context.HttpContext.TraceIdentifier // Unique request ID
        };

        // Handle different exception types
        switch (context.Exception)
        {
            case ArgumentNullException argNullEx:
                context.HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
                response.StatusCode = 400;
                response.Message = "Required field is missing";
                response.Detail = argNullEx.Message;
                break;

            case InvalidOperationException invalidOpEx:
                context.HttpContext.Response.StatusCode = StatusCodes.Status409Conflict;
                response.StatusCode = 409;
                response.Message = "Operation cannot be performed";
                response.Detail = invalidOpEx.Message;
                break;

            default:
                context.HttpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
                response.StatusCode = 500;
                response.Message = "Internal server error";
                break;
        }
        // Mark exception as handled (prevents default error page)
        context.Result = new JsonResult(response);
        context.ExceptionHandled = true;
    }
}

public class ErrorResponse
{
    public int StatusCode { get; set; }
    public string Message { get; set; }
    public string Detail { get; set; }
    public string TraceId { get; set; } // Unique identifier for tracking
}

Registration Methods

Register in Program.cs:

M-1: Global Registration (all Controllers)

builder.Services.AddControllers(options =>
{
    // Add filter globally to all controllers
    options.Filters.Add<CustomExceptionFilter>();
});

Or use as attribute on specific controller/action:

M-2: Controller-Level Attribute

[ApiController]
[ServiceFilter(typeof(CustomExceptionFilter))]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    [HttpGet("{id}")]
    public IActionResult GetUser(int id)
    {
        var user = _userService.GetUserById(id) 
            ?? throw new KeyNotFoundException("User not found");
        return Ok(user);
    }
}

// M3: - Action level Attribute
[ServiceFilter(typeof(CustomExceptionFilter))] // Only this action uses filter

When to Use:

  • Custom exception handling for specific controllers/actions
  • Different error responses for different endpoints
  • When you need more control than global middleware but less than try-catch

Pros:

  • Flexible - can be applied globally or to specific controllers/actions
  • Cleaner than scattered try-catch blocks
  • Can inherit and extend for different use cases
  • Better for cross-cutting concerns

Cons:

  • More complex setup than try-catch
  • May conflict with global middleware
  • Still requires registration

Execution Flow

Request arrives
    ↓
Routing matches controller/action
    ↓
Is Exception Filter attribute present?
    ↙              ↘
  Yes              No
    ↓              ↓
Use Filter     Skip Filter
    ↓              ↓
Controller Action executes
    ↓
Exception thrown?
    ↙           ↘
  No            Yes
    ↓            ↓
Response      OnException() called
sent          in filter
    ↓            ↓
            Set Status Code
                ↓
            Create response
                ↓
            Return JsonResult
                ↓
            Response sent

4. Problem Details (RFC 7807)

Standard way to return error information in HTTP API responses.

Implementation:

Using Built-in ProblemDetails:

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IProductService _productService;
    private readonly ILogger<ProductsController> _logger;

    public ProductsController(IProductService productService, ILogger<ProductsController> logger)
    {
        _productService = productService;
        _logger = logger;
    }

    [HttpGet("{id}")]
    public IActionResult GetProduct(int id)
    {
        try
        {
            if (id <= 0)
            {
                var problemDetails = new ProblemDetails
                {
                    Status = StatusCodes.Status400BadRequest,
                    Title = "Invalid Input",
                    Detail = "Product ID must be greater than 0",
                    Instance = HttpContext.Request.Path
                };
                return BadRequest(problemDetails);
            }

            var product = _productService.GetProductById(id);
            
            if (product == null)
            {
                var problemDetails = new ProblemDetails
                {
                    Status = StatusCodes.Status404NotFound,
                    Title = "Not Found",
                    Detail = $"Product with ID {id} not found",
                    Instance = HttpContext.Request.Path
                };
                return NotFound(problemDetails);
            }

            return Ok(product);
        }
        catch (InvalidOperationException ex)
        {
            _logger.LogError(ex, "Invalid operation while getting product");
            var problemDetails = new ProblemDetails
            {
                Status = StatusCodes.Status409Conflict,
                Title = "Operation Failed",
                Detail = ex.Message,
                Instance = HttpContext.Request.Path
            };
            return Conflict(problemDetails);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Unexpected error while getting product");
            var problemDetails = new ProblemDetails
            {
                Status = StatusCodes.Status500InternalServerError,
                Title = "Internal Server Error",
                Detail = "An unexpected error occurred",
                Instance = HttpContext.Request.Path
            };
            return StatusCode(StatusCodes.Status500InternalServerError, problemDetails);
        }
    }
}

Custom ProblemDetails:

public class CustomProblemDetails : ProblemDetails
{
    public string[] Errors { get; set; }
    public DateTime Timestamp { get; set; } = DateTime.UtcNow;
}

Configure Problem Details Middleware:

builder.Services.AddProblemDetails(options =>
{
    options.CustomizeProblemDetails = context =>
    {
        var problemDetails = context.ProblemDetails;
        
        if (!context.HttpContext.Items.ContainsKey("errors"))
            return;

        var errors = context.HttpContext.Items["errors"] as string[];
        
        var customDetails = new CustomProblemDetails
        {
            Status = problemDetails.Status,
            Title = problemDetails.Title,
            Detail = problemDetails.Detail,
            Instance = problemDetails.Instance,
            Errors = errors,
            Type = problemDetails.Type
        };

        context.HttpContext.Response.StatusCode = problemDetails.Status.Value;
        context.ProblemDetails = customDetails;
    };
});

app.UseExceptionHandler();

When to Use:

  • Standard API responses following RFC 7807
  • When building APIs that follow industry standards
  • For better integration with API clients and documentation
  • Production APIs that need consistent error format

Pros:

  • Industry standard (RFC 7807)
  • Tools recognize and parse automatically
  • Consistent format for all error responses
  • Better API documentation and client handling

Cons:

  • Slightly more verbose than simple error objects
  • Requires proper configuration

5. Custom Exception Classes

Create specific exception types for different error scenarios.

Implementation:

Define Custom Exceptions:

// Base custom exception
public class ApiException : Exception
{
    public int StatusCode { get; set; }
    public string ErrorCode { get; set; }

    public ApiException(string message, int statusCode = 500, string errorCode = null)
        : base(message)
    {
        StatusCode = statusCode;
        ErrorCode = errorCode ?? "INTERNAL_ERROR";
    }
}

// Specific exceptions
public class NotFoundException : ApiException
{
    public NotFoundException(string message, string errorCode = "NOT_FOUND")
        : base(message, 404, errorCode)
    {
    }
}

public class BadRequestException : ApiException
{
    public BadRequestException(string message, string errorCode = "BAD_REQUEST")
        : base(message, 400, errorCode)
    {
    }
}

public class UnauthorizedException : ApiException
{
    public UnauthorizedException(string message = "Unauthorized access", 
        string errorCode = "UNAUTHORIZED")
        : base(message, 401, errorCode)
    {
    }
}

public class ValidationException : ApiException
{
    public IDictionary<string, string[]> Errors { get; }

    public ValidationException(IDictionary<string, string[]> errors)
        : base("One or more validation errors occurred", 400, "VALIDATION_ERROR")
    {
        Errors = errors;
    }
}

public class ConflictException : ApiException
{
    public ConflictException(string message, string errorCode = "CONFLICT")
        : base(message, 409, errorCode)
    {
    }
}

Using Custom Exceptions:

[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
    private readonly IOrderService _orderService;

    public OrdersController(IOrderService orderService)
    {
        _orderService = orderService;
    }

    [HttpPost]
    public async Task<IActionResult> CreateOrder([FromBody] CreateOrderRequest request)
    {
        try
        {
            if (request == null)
                throw new BadRequestException("Request body cannot be empty");

            if (string.IsNullOrWhiteSpace(request.OrderNumber))
                throw new BadRequestException("Order number is required", "MISSING_ORDER_NUMBER");

            if (request.Items == null || request.Items.Count == 0)
                throw new BadRequestException("Order must contain at least one item");

            var order = await _orderService.CreateOrderAsync(request);
            
            return CreatedAtAction(nameof(GetOrder), new { id = order.Id }, order);
        }
        catch (ValidationException ex)
        {
            var problemDetails = new ProblemDetails
            {
                Status = 400,
                Title = "Validation Error",
                Detail = ex.Message,
            };
            return BadRequest(problemDetails);
        }
    }

    [HttpGet("{id}")]
    public async Task<IActionResult> GetOrder(int id)
    {
        try
        {
            var order = await _orderService.GetOrderByIdAsync(id)
                ?? throw new NotFoundException($"Order with ID {id} not found", "ORDER_NOT_FOUND");

            return Ok(order);
        }
        catch (ApiException ex)
        {
            var problemDetails = new ProblemDetails
            {
                Status = ex.StatusCode,
                Title = ex.GetType().Name,
                Detail = ex.Message,
            };
            return StatusCode(ex.StatusCode, problemDetails);
        }
    }
}

Global Handler for Custom Exceptions:

public class ApiExceptionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ApiExceptionMiddleware> _logger;

    public ApiExceptionMiddleware(RequestDelegate next, ILogger<ApiExceptionMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (ApiException ex)
        {
            _logger.LogWarning(ex, "API Exception: {ErrorCode}", ex.ErrorCode);
            await HandleApiExceptionAsync(context, ex);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Unexpected exception");
            await HandleUnexpectedExceptionAsync(context, ex);
        }
    }

    private static Task HandleApiExceptionAsync(HttpContext context, ApiException ex)
    {
        context.Response.ContentType = "application/json";
        context.Response.StatusCode = ex.StatusCode;

        var response = new ErrorResponse
        {
            StatusCode = ex.StatusCode,
            Message = ex.Message,
            ErrorCode = ex.ErrorCode,
            TraceId = context.TraceIdentifier
        };

        return context.Response.WriteAsJsonAsync(response);
    }

    private static Task HandleUnexpectedExceptionAsync(HttpContext context, Exception ex)
    {
        context.Response.ContentType = "application/json";
        context.Response.StatusCode = StatusCodes.Status500InternalServerError;

        var response = new ErrorResponse
        {
            StatusCode = 500,
            Message = "Internal server error",
            ErrorCode = "INTERNAL_ERROR",
            TraceId = context.TraceIdentifier
        };

        return context.Response.WriteAsJsonAsync(response);
    }
}

public class ErrorResponse
{
    public int StatusCode { get; set; }
    public string Message { get; set; }
    public string ErrorCode { get; set; }
    public string TraceId { get; set; }
}

When to Use:

  • Clear, semantic error handling
  • Different exception types for different scenarios
  • When you need error codes and specific status codes
  • For better error tracking and logging

Pros:

  • Clear intent and meaning
  • Easy to catch specific exception types
  • Can include custom properties
  • Better for error tracking systems

Cons:

  • Requires custom class definitions
  • More complex exception hierarchy management

6. Validation Filters with Automatic Error Response

Automatic validation error response for model binding errors.

Implementation:

Model Validation Filter:

public class ValidationFilter : IActionFilter
{
    public void OnActionExecuting(ActionExecutingContext context)
    {
        if (!context.ModelState.IsValid)
        {
            var errors = context.ModelState
                .Where(x => x.Value.Errors.Count > 0)
                .ToDictionary(
                    kvp => kvp.Key,
                    kvp => kvp.Value.Errors.Select(e => e.ErrorMessage).ToArray()
                );

            var problemDetails = new ValidationProblemDetails(errors)
            {
                Status = StatusCodes.Status400BadRequest,
                Title = "One or more validation errors occurred"
            };

            context.Result = new BadRequestObjectResult(problemDetails);
        }
    }

    public void OnActionExecuted(ActionExecutedContext context)
    {
        // Do nothing
    }
}

Register in Program.cs:

builder.Services.AddControllers(options =>
{
    options.Filters.Add<ValidationFilter>();
});

// Also disable automatic model state validation
builder.Services.Configure<ApiBehaviorOptions>(options =>
{
    options.SuppressModelStateInvalidFilter = false;
});

Usage in Controller:

[HttpPost]
public IActionResult CreateUser([FromBody] CreateUserRequest request)
{
    // If model is invalid, ValidationFilter automatically returns 400
    // No need for manual validation
    var user = _userService.CreateUser(request);
    return CreatedAtAction(nameof(GetUser), new { id = user.Id }, user);
}

When to Use:

  • Automatic validation of model binding errors
  • Consistent validation error responses
  • Reducing boilerplate validation code in controllers

Pros:

  • Automatic validation handling
  • Consistent error format
  • Cleaner controller code

Cons:

  • Global application, affects all endpoints
  • Less control over specific validations

When to Use Each Method

MethodUse CaseComplexityScope
Try-CatchSimple, localized error handlingLowSingle method
Global MiddlewareCentralized, app-wide error handlingMediumEntire application
Exception FiltersSpecific controller/action handlingMediumController/Action level
Problem DetailsStandard REST error responsesMediumSpecific endpoints
Custom ExceptionsSemantic, specific error scenariosMediumEntire application
Validation FiltersAutomatic model validation errorsLowModel binding level

Best Practice Combination:

For production applications, combine multiple approaches:

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Add services
builder.Services.AddControllers(options =>
{
    options.Filters.Add<ValidationFilter>();
    options.Filters.Add<CustomExceptionFilter>();
});

builder.Services.AddScoped<CustomExceptionFilter>();
builder.Services.AddLogging();

var app = builder.Build();

// Add middleware
app.UseMiddleware<GlobalExceptionMiddleware>();
app.UseExceptionHandler();

app.UseRouting();
app.MapControllers();

app.Run();

Interview Questions (3+ Years)

Question 1: What is the difference between try-catch blocks and middleware for error handling?

Answer:

AspectTry-CatchMiddleware
ScopeLocal to methodGlobal to application
GranularityPer methodAll requests
ReusabilityRepetitive if used multiple timesSingle implementation
PerformanceMinimal overheadSlight overhead per request
ComplexitySimple but repetitiveMore complex but centralized

Try-Catch Example:

[HttpGet("{id}")]
public IActionResult GetUser(int id)
{
    try
    {
        // Business logic
        return Ok(user);
    }
    catch (Exception ex)
    {
        // Handle locally
        return StatusCode(500, ex.Message);
    }
}

Middleware Example:

public class ErrorMiddleware
{
    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            // Handle all requests
            await HandleException(context, ex);
        }
    }
}

Best Practice: Use middleware for unhandled exceptions and global error handling, use try-catch for specific business logic with custom handling.


Question 2: How would you implement error logging and ensure sensitive data is not logged?

Answer:

public class SecureLoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<SecureLoggingMiddleware> _logger;
    // List of sensitive fields to exclude
    private static readonly string[] SensitiveFields = 
    { 
        "password", "token", "apikey", "secret", "creditcard", "ssn" 
    };

    public SecureLoggingMiddleware(RequestDelegate next, ILogger<SecureLoggingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // Log request without sensitive data
        await LogRequestAsync(context);

        var originalBodyStream = context.Response.Body;
        using (var responseBody = new MemoryStream())
        {
            context.Response.Body = responseBody;

            try
            {
                await _next(context);
                // Log response
                await LogResponseAsync(context);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "An error occurred: {ExceptionMessage}", 
                    MaskSensitiveData(ex.Message));
                throw;
            }
            finally
            {
                await responseBody.CopyToAsync(originalBodyStream);
            }
        }
    }

    private async Task LogRequestAsync(HttpContext context)
    {
        context.Request.EnableBuffering();
        var body = await new StreamReader(context.Request.Body).ReadToEndAsync();
        
        var maskedBody = MaskSensitiveData(body);
        
        _logger.LogInformation(
            "HTTP {Method} {Path} | Query: {Query} | Body: {Body}",
            context.Request.Method,
            context.Request.Path,
            MaskSensitiveData(context.Request.QueryString.ToString()),
            maskedBody
        );

        context.Request.Body.Position = 0;
    }

    private async Task LogResponseAsync(HttpContext context)
    {
        context.Response.Body.Seek(0, SeekOrigin.Begin);
        var body = await new StreamReader(context.Response.Body).ReadToEndAsync();
        
        var maskedBody = MaskSensitiveData(body);
        
        _logger.LogInformation(
            "HTTP Response {StatusCode} | Body: {Body}",
            context.Response.StatusCode,
            maskedBody
        );

        context.Response.Body.Seek(0, SeekOrigin.Begin);
    }

    private string MaskSensitiveData(string data)
    {
        if (string.IsNullOrEmpty(data)) return data;

        var result = data;
        foreach (var field in SensitiveFields)
        {
            result = System.Text.RegularExpressions.Regex.Replace(
                result,
                $"(\"{field}\"\\s*:\\s*\")([^\"]+)(\")",
                "$1***MASKED***$3",
                System.Text.RegularExpressions.RegexOptions.IgnoreCase
            );
        }

        return result;
    }
}

Best Practices:

  • Never log passwords, tokens, API keys
  • Use structured logging (Serilog, NLog)
  • Implement log redaction for sensitive fields
  • Use logging levels appropriately (Info, Warning, Error)
  • Store logs securely

Question 3: What is RFC 7807 Problem Details and how does it improve API error handling?

Answer:

RFC 7807 is a standard for returning error information in HTTP API responses. It provides a consistent format for clients to understand errors.

Standard Format:

{
  "type": "https://example.com/errors/invalid-age",
  "title": "Your age is invalid",
  "status": 400,
  "detail": "Age must be at least 18 and less than 150",
  "instance": "/account/12345/messages/abc",
  "errors": {
    "age": ["must be >= 18", "must be < 150"]
  }
}

Implementation:

[HttpPost]
public IActionResult ValidateAge([FromBody] AgeRequest request)
{
    var errors = new Dictionary<string, string[]>();

    if (request.Age < 18)
        errors["age"] = new[] { "Age must be at least 18" };

    if (request.Age > 150)
        errors["age"] = new[] { "Age must be less than 150" };

    if (errors.Count > 0)
    {
        var problemDetails = new ValidationProblemDetails(errors)
        {
            Type = "https://example.com/errors/validation-error",
            Title = "Validation Failed",
            Status = StatusCodes.Status400BadRequest,
            Detail = "One or more validation errors occurred",
            Instance = HttpContext.Request.Path
        };

        return BadRequest(problemDetails);
    }

    return Ok(new { message = "Valid age" });
}

Advantages:

  1. Standardization: All clients understand the format
  2. Rich Information: Type, title, status, detail, instance
  3. Extensibility: Can add custom properties
  4. Tool Support: API documentation generators recognize the format
  5. Client Friendly: Better error handling in client applications

Question 4: How would you handle different exception types differently in global error handling?

Answer:

public class SmartExceptionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<SmartExceptionMiddleware> _logger;

    public SmartExceptionMiddleware(RequestDelegate next, ILogger<SmartExceptionMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            await HandleExceptionAsync(context, ex);
        }
    }

    private Task HandleExceptionAsync(HttpContext context, Exception ex)
    {
        context.Response.ContentType = "application/json";

        var response = ex switch
        {
            // Validation exceptions - 400
            ArgumentException or ArgumentNullException or FormatException =>
                HandleValidationException(context, ex),

            // Not found - 404
            KeyNotFoundException => 
                HandleNotFoundException(context, ex),

            // Unauthorized - 401
            UnauthorizedAccessException => 
                HandleUnauthorizedException(context, ex),

            // Conflict - 409
            InvalidOperationException => 
                HandleConflictException(context, ex),

            // Custom exception - check status code
            ApiException apiEx =>
                HandleApiException(context, apiEx),

            // Unexpected - 500
            _ => HandleInternalServerError(context, ex)
        };

        return response;
    }

    private Task HandleValidationException(HttpContext context, Exception ex)
    {
        context.Response.StatusCode = StatusCodes.Status400BadRequest;
        _logger.LogWarning(ex, "Validation error: {Message}", ex.Message);

        var errorResponse = new ErrorResponse
        {
            StatusCode = 400,
            Message = "Validation failed",
            Detail = ex.Message,
            TraceId = context.TraceIdentifier
        };

        return context.Response.WriteAsJsonAsync(errorResponse);
    }

    private Task HandleNotFoundException(HttpContext context, Exception ex)
    {
        context.Response.StatusCode = StatusCodes.Status404NotFound;
        _logger.LogWarning("Resource not found: {Message}", ex.Message);

        var errorResponse = new ErrorResponse
        {
            StatusCode = 404,
            Message = "Resource not found",
            Detail = ex.Message,
            TraceId = context.TraceIdentifier
        };

        return context.Response.WriteAsJsonAsync(errorResponse);
    }

    private Task HandleUnauthorizedException(HttpContext context, Exception ex)
    {
        context.Response.StatusCode = StatusCodes.Status401Unauthorized;
        _logger.LogWarning("Unauthorized access attempt");

        var errorResponse = new ErrorResponse
        {
            StatusCode = 401,
            Message = "Unauthorized access",
            TraceId = context.TraceIdentifier
        };

        return context.Response.WriteAsJsonAsync(errorResponse);
    }

    private Task HandleConflictException(HttpContext context, Exception ex)
    {
        context.Response.StatusCode = StatusCodes.Status409Conflict;
        _logger.LogWarning(ex, "Conflict error: {Message}", ex.Message);

        var errorResponse = new ErrorResponse
        {
            StatusCode = 409,
            Message = "Operation conflict",
            Detail = ex.Message,
            TraceId = context.TraceIdentifier
        };

        return context.Response.WriteAsJsonAsync(errorResponse);
    }

    private Task HandleApiException(HttpContext context, ApiException ex)
    {
        context.Response.StatusCode = ex.StatusCode;
        var level = ex.StatusCode >= 500 ? LogLevel.Error : LogLevel.Warning;
        _logger.Log(level, ex, "API Exception: {ErrorCode}", ex.ErrorCode);

        var errorResponse = new ErrorResponse
        {
            StatusCode = ex.StatusCode,
            Message = ex.Message,
            ErrorCode = ex.ErrorCode,
            TraceId = context.TraceIdentifier
        };

        return context.Response.WriteAsJsonAsync(errorResponse);
    }

    private Task HandleInternalServerError(HttpContext context, Exception ex)
    {
        context.Response.StatusCode = StatusCodes.Status500InternalServerError;
        _logger.LogError(ex, "Unhandled exception occurred");

        var errorResponse = new ErrorResponse
        {
            StatusCode = 500,
            Message = "Internal server error",
            Detail = "An unexpected error occurred. Please contact support.",
            TraceId = context.TraceIdentifier
        };

        return context.Response.WriteAsJsonAsync(errorResponse);
    }
}

public class ErrorResponse
{
    public int StatusCode { get; set; }
    public string Message { get; set; }
    public string Detail { get; set; }
    public string ErrorCode { get; set; }
    public string TraceId { get; set; }
}

public class ApiException : Exception
{
    public int StatusCode { get; set; }
    public string ErrorCode { get; set; }

    public ApiException(string message, int statusCode = 500, string errorCode = null)
        : base(message)
    {
        StatusCode = statusCode;
        ErrorCode = errorCode ?? "INTERNAL_ERROR";
    }
}

Key Points:

  • Use switch expressions for cleaner pattern matching
  • Log at appropriate levels (Warning vs Error)
  • Return different HTTP status codes based on exception type
  • Never expose internal details in production
  • Include trace ID for debugging

Question 5: What's the best practice for handling async exceptions in Web API?

Answer:

Challenge: Exceptions in async code can be tricky if not handled properly.

// ❌ WRONG - Task exception not caught
[HttpGet("{id}")]
public Task<IActionResult> GetUserWrong(int id)
{
    return _userService.GetUserAsync(id)
        .ContinueWith(task => Ok(task.Result)); // Exception not handled
}

// ✅ CORRECT - Proper async/await with try-catch
[HttpGet("{id}")]
public async Task<IActionResult> GetUser(int id)
{
    try
    {
        var user = await _userService.GetUserAsync(id);
        
        if (user == null)
            return NotFound();

        return Ok(user);
    }
    catch (OperationCanceledException ex)
    {
        _logger.LogWarning(ex, "Request cancelled");
        return StatusCode(StatusCodes.Status408RequestTimeout);
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Error retrieving user");
        return StatusCode(StatusCodes.Status500InternalServerError);
    }
}

// ✅ BEST - Using middleware that handles async exceptions
public class AsyncExceptionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<AsyncExceptionMiddleware> _logger;

    public AsyncExceptionMiddleware(RequestDelegate next, ILogger<AsyncExceptionMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (OperationCanceledException ex)
        {
            _logger.LogWarning(ex, "Request was cancelled");
            context.Response.StatusCode = StatusCodes.Status408RequestTimeout;
            await context.Response.WriteAsJsonAsync(new { message = "Request timeout" });
        }
        catch (TaskCanceledException ex)
        {
            _logger.LogWarning(ex, "Task was cancelled");
            context.Response.StatusCode = StatusCodes.Status408RequestTimeout;
            await context.Response.WriteAsJsonAsync(new { message = "Request timeout" });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Unhandled exception");
            context.Response.StatusCode = StatusCodes.Status500InternalServerError;
            await context.Response.WriteAsJsonAsync(new { message = "Internal server error" });
        }
    }
}

// Async service with proper error handling
public class UserService : IUserService
{
    private readonly IUserRepository _repository;
    private readonly ILogger<UserService> _logger;

    public UserService(IUserRepository repository, ILogger<UserService> logger)
    {
        _repository = repository;
        _logger = logger;
    }

    public async Task<User> GetUserAsync(int id)
    {
        try
        {
            if (id <= 0)
                throw new ArgumentException("User ID must be greater than 0", nameof(id));

            // Use CancellationToken for timeout control
            using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
            {
                return await _repository.GetUserByIdAsync(id, cts.Token);
            }
        }
        catch (OperationCanceledException ex)
        {
            _logger.LogWarning(ex, "GetUserAsync timeout for ID {UserId}", id);
            throw;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error retrieving user {UserId}", id);
            throw;
        }
    }

    public async Task<List<User>> GetUsersAsync(CancellationToken cancellationToken = default)
    {
        try
        {
            return await _repository.GetAllUsersAsync(cancellationToken);
        }
        catch (OperationCanceledException ex)
        {
            _logger.LogWarning(ex, "GetUsersAsync was cancelled");
            throw;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error retrieving users");
            throw;
        }
    }
}

Best Practices for Async Exceptions:

  1. Always use async/await instead of ContinueWith
  2. Handle OperationCanceledException and TaskCanceledException separately
  3. Use CancellationToken for timeout control
  4. Use global middleware to catch unhandled async exceptions
  5. Log at appropriate levels
  6. Don't hide exception details with generic responses

Question 6: How would you implement circuit breaker pattern for external API calls error handling?

Answer:

// Using Polly library for circuit breaker
public interface IExternalApiService
{
    Task<ExternalData> GetDataAsync(int id);
}

public class ExternalApiService : IExternalApiService
{
    private readonly HttpClient _httpClient;
    private readonly IAsyncPolicy<HttpResponseMessage> _policy;
    private readonly ILogger<ExternalApiService> _logger;

    public ExternalApiService(HttpClient httpClient, ILogger<ExternalApiService> logger)
    {
        _httpClient = httpClient;
        _logger = logger;

        // Configure circuit breaker policy
        var circuitBreakerPolicy = Policy
            .Handle<HttpRequestException>()
            .Or<TaskCanceledException>()
            .OrResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
            .CircuitBreakerAsync<HttpResponseMessage>(
                handledEventsAllowedBeforeBreaking: 3,
                durationOfBreak: TimeSpan.FromSeconds(10),
                onBreak: (outcome, duration) =>
                {
                    _logger.LogWarning(
                        "Circuit breaker opened for {Duration} seconds",
                        duration.TotalSeconds
                    );
                },
                onReset: () =>
                {
                    _logger.LogInformation("Circuit breaker reset");
                }
            );

        // Configure retry policy
        var retryPolicy = Policy
            .Handle<HttpRequestException>()
            .Or<TaskCanceledException>()
            .OrResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
            .WaitAndRetryAsync<HttpResponseMessage>(
                retryCount: 2,
                sleepDurationProvider: retryAttempt =>
                    TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
                onRetry: (outcome, timespan, retryCount, context) =>
                {
                    _logger.LogWarning(
                        "Retry {RetryCount} after {Seconds} seconds",
                        retryCount,
                        timespan.TotalSeconds
                    );
                }
            );

        // Combine policies
        _policy = Policy.WrapAsync<HttpResponseMessage>(retryPolicy, circuitBreakerPolicy);
    }

    public async Task<ExternalData> GetDataAsync(int id)
    {
        try
        {
            var response = await _policy.ExecuteAsync(async () =>
            {
                return await _httpClient.GetAsync($"https://api.example.com/data/{id}");
            });

            if (!response.IsSuccessStatusCode)
            {
                _logger.LogError(
                    "External API returned {StatusCode}",
                    response.StatusCode
                );
                throw new ExternalApiException(
                    $"External API error: {response.StatusCode}"
                );
            }

            var json = await response.Content.ReadAsStringAsync();
            var data = JsonSerializer.Deserialize<ExternalData>(json);

            return data;
        }
        catch (BrokenCircuitException ex)
        {
            _logger.LogError(ex, "Circuit breaker is open");
            throw new ServiceUnavailableException("External service temporarily unavailable");
        }
        catch (HttpRequestException ex)
        {
            _logger.LogError(ex, "HTTP request to external API failed");
            throw new ExternalApiException("Failed to connect to external service", ex);
        }
    }
}

// Custom exceptions
public class ExternalApiException : Exception
{
    public ExternalApiException(string message, Exception innerException = null)
        : base(message, innerException) { }
}

public class ServiceUnavailableException : Exception
{
    public ServiceUnavailableException(string message) : base(message) { }
}

// Register in Program.cs
builder.Services.AddHttpClient<IExternalApiService, ExternalApiService>()
    .ConfigureHttpClient(client =>
    {
        client.BaseAddress = new Uri("https://api.example.com");
        client.Timeout = TimeSpan.FromSeconds(10);
    });

// Usage in controller
[ApiController]
[Route("api/[controller]")]
public class DataController : ControllerBase
{
    private readonly IExternalApiService _externalApiService;

    public DataController(IExternalApiService externalApiService)
    {
        _externalApiService = externalApiService;
    }

    [HttpGet("{id}")]
    public async Task<IActionResult> GetData(int id)
    {
        try
        {
            var data = await _externalApiService.GetDataAsync(id);
            return Ok(data);
        }
        catch (ServiceUnavailableException ex)
        {
            return StatusCode(StatusCodes.Status503ServiceUnavailable,
                new { message = ex.Message });
        }
        catch (ExternalApiException ex)
        {
            return StatusCode(StatusCodes.Status502BadGateway,
                new { message = "External service error" });
        }
    }
}

Circuit Breaker Advantages:

  • Prevents cascading failures
  • Gives external services time to recover
  • Provides quick failure responses
  • Improves system resilience

Question 7: Explain the difference between exceptions and status codes. When should you use each?

Answer:

Exceptions are for exceptional program flow (errors), while Status Codes are for communicating the result of an HTTP request.

[HttpPost]
public async Task<IActionResult> CreateUser([FromBody] CreateUserRequest request)
{
    // ❌ WRONG - Don't throw exception for validation failure
    if (string.IsNullOrEmpty(request.Email))
        throw new Exception("Email is required"); // This is a bad request, not an exception

    // ✅ CORRECT - Return 400 BadRequest for validation failure
    if (string.IsNullOrEmpty(request.Email))
        return BadRequest(new { message = "Email is required" });

    try
    {
        // ✅ CORRECT - Throw exception for unexpected errors
        if (!await _emailService.IsValidAsync(request.Email))
            throw new InvalidOperationException("Email validation service failed");

        var user = await _userService.CreateUserAsync(request);
        
        // ✅ CORRECT - Return 201 Created for successful creation
        return CreatedAtAction(nameof(GetUser), new { id = user.Id }, user);
    }
    catch (InvalidOperationException ex)
    {
        // ❌ Caught unexpected exception, return 500
        return StatusCode(StatusCodes.Status500InternalServerError,
            new { message = "An unexpected error occurred" });
    }
}

// Status Code Guide
public class StatusCodeGuide
{
    // 2xx Success
    public void Success()
    {
        // 200 OK - Successful GET, PUT, PATCH
        // 201 Created - Successful POST
        // 204 No Content - Successful DELETE
    }

    // 4xx Client Error
    public void ClientError()
    {
        // 400 Bad Request - Invalid input, validation failed
        // 401 Unauthorized - Authentication failed
        // 403 Forbidden - Authenticated but not authorized
        // 404 Not Found - Resource doesn't exist
        // 409 Conflict - Business logic conflict (duplicate, invalid state)
        // 422 Unprocessable Entity - Request well-formed but contains semantic errors
    }

    // 5xx Server Error
    public void ServerError()
    {
        // 500 Internal Server Error - Unexpected server error
        // 502 Bad Gateway - External service error
        // 503 Service Unavailable - Server temporarily unavailable
    }
}

// ✅ CORRECT Pattern
[HttpPut("{id}")]
public async Task<IActionResult> UpdateUser(int id, [FromBody] UpdateUserRequest request)
{
    // Validation - Return 400
    if (id <= 0)
        return BadRequest(new { message = "Invalid ID" });

    // Check existence - Return 404
    var user = await _userService.GetUserByIdAsync(id);
    if (user == null)
        return NotFound(new { message = "User not found" });

    // Check authorization - Return 403
    if (!await _authService.CanUpdateUserAsync(CurrentUser, id))
        return Forbid();

    try
    {
        // Business logic that might fail - Throw exception
        await _userService.UpdateUserAsync(id, request);
        
        // Success - Return 200 OK
        return Ok(new { message = "User updated successfully" });
    }
    catch (InvalidOperationException ex)
    {
        // Conflict - Return 409
        return Conflict(new { message = ex.Message });
    }
    catch (Exception ex)
    {
        // Unexpected error - Return 500
        return StatusCode(StatusCodes.Status500InternalServerError,
            new { message = "An unexpected error occurred" });
    }
}

When to Use Each:

ScenarioUseStatus Code
Missing required fieldStatus Code400 Bad Request
Invalid data formatStatus Code400 Bad Request
User not authenticatedStatus Code401 Unauthorized
Resource not foundStatus Code404 Not Found
Business rule violationStatus Code409 Conflict
Database connection failedException → Status Code500 Internal Error
External service timeoutException → Status Code502/503
Unexpected null referenceException → Status Code500 Internal Error

Question 8: How would you monitor and track errors in production?

Answer:

// Setup with Serilog and Application Insights
public static class LoggingConfiguration
{
    public static void ConfigureLogging(WebApplicationBuilder builder)
    {
        var logger = new LoggerConfiguration()
            .MinimumLevel.Information()
            .Enrich.FromLogContext()
            .Enrich.WithProperty("ApplicationName", "MyWebAPI")
            .Enrich.WithMachineName()
            .WriteTo.Console()
            .WriteTo.File(
                path: "logs/myapi-.txt",
                rollingInterval: RollingInterval.Day,
                retainedFileCountLimit: 30,
                outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}"
            )
            .WriteTo.ApplicationInsights(
                new TelemetryConfiguration { InstrumentationKey = builder.Configuration["ApplicationInsights:InstrumentationKey"] },
                TelemetryConverter.Traces
            )
            .CreateLogger();

        Log.Logger = logger;
        builder.Logging.AddSerilog(logger);
    }
}

// Structured error logging middleware
public class ErrorTrackingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ErrorTrackingMiddleware> _logger;
    private readonly TelemetryClient _telemetryClient;

    public ErrorTrackingMiddleware(RequestDelegate next, ILogger<ErrorTrackingMiddleware> logger, 
        TelemetryClient telemetryClient)
    {
        _next = next;
        _logger = logger;
        _telemetryClient = telemetryClient;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            await HandleExceptionAsync(context, ex);
        }
    }

    private Task HandleExceptionAsync(HttpContext context, Exception ex)
    {
        var correlationId = context.TraceIdentifier;
        
        // Structured logging
        _logger.LogError(ex,
            "Error occurred | CorrelationId: {CorrelationId} | Method: {Method} | Path: {Path} | Query: {Query}",
            correlationId,
            context.Request.Method,
            context.Request.Path,
            context.Request.QueryString
        );

        // Track to Application Insights
        var properties = new Dictionary<string, string>
        {
            { "CorrelationId", correlationId },
            { "Method", context.Request.Method },
            { "Path", context.Request.Path.ToString() },
            { "StatusCode", context.Response.StatusCode.ToString() }
        };

        var metrics = new Dictionary<string, double>
        {
            { "ErrorOccurred", 1 }
        };

        _telemetryClient.TrackEvent("ApplicationError", properties, metrics);
        _telemetryClient.TrackException(ex, properties);

        context.Response.ContentType = "application/json";
        context.Response.StatusCode = StatusCodes.Status500InternalServerError;

        return context.Response.WriteAsJsonAsync(new ErrorResponse
        {
            Message = "An error occurred",
            TraceId = correlationId,
            Timestamp = DateTime.UtcNow
        });
    }
}

public class ErrorResponse
{
    public string Message { get; set; }
    public string TraceId { get; set; }
    public DateTime Timestamp { get; set; }
}

// Register in Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddApplicationInsightsTelemetry();
LoggingConfiguration.ConfigureLogging(builder);

var app = builder.Build();
app.UseMiddleware<ErrorTrackingMiddleware>();

Monitoring Best Practices:

  1. Use structured logging (Serilog, NLog)
  2. Include correlation IDs for request tracking
  3. Send to centralized logging (Application Insights, ELK Stack)
  4. Set up alerts for error thresholds
  5. Monitor error rates and patterns
  6. Log enough context but not sensitive data

Summary

Key Takeaways:

  1. Multiple approaches exist for error handling in Web API
  2. Combine methods for comprehensive error handling
  3. Use middleware for global exception handling
  4. Follow RFC 7807 for standard error responses
  5. Create custom exceptions for semantic errors
  6. Log appropriately with structured logging
  7. Handle async errors with proper async/await
  8. Monitor in production with proper tools

Recommended Production Setup:

// Program.cs
builder.Services.AddControllers(options =>
{
    options.Filters.Add<ValidationFilter>();
    options.Filters.Add<CustomExceptionFilter>();
});

builder.Services.AddProblemDetails();
builder.Services.AddApplicationInsightsTelemetry();
LoggingConfiguration.ConfigureLogging(builder);

var app = builder.Build();

app.UseMiddleware<GlobalExceptionMiddleware>();
app.UseMiddleware<ErrorTrackingMiddleware>();
app.UseExceptionHandler();

app.UseRouting();
app.MapControllers();

app.Run();

This provides multiple layers of error handling, comprehensive logging, and production-ready monitoring.

Comments

Popular posts from this blog