Error Handling_ in_ Web API
Error Handling in ASP.NET Core Web API - Complete Guide
Table of Contents
- Overview
- Error Handling Methods
- Implementation Examples
- When to Use Each Method
- 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
| Method | Use Case | Complexity | Scope |
|---|---|---|---|
| Try-Catch | Simple, localized error handling | Low | Single method |
| Global Middleware | Centralized, app-wide error handling | Medium | Entire application |
| Exception Filters | Specific controller/action handling | Medium | Controller/Action level |
| Problem Details | Standard REST error responses | Medium | Specific endpoints |
| Custom Exceptions | Semantic, specific error scenarios | Medium | Entire application |
| Validation Filters | Automatic model validation errors | Low | Model 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:
| Aspect | Try-Catch | Middleware |
|---|---|---|
| Scope | Local to method | Global to application |
| Granularity | Per method | All requests |
| Reusability | Repetitive if used multiple times | Single implementation |
| Performance | Minimal overhead | Slight overhead per request |
| Complexity | Simple but repetitive | More 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:
- Standardization: All clients understand the format
- Rich Information: Type, title, status, detail, instance
- Extensibility: Can add custom properties
- Tool Support: API documentation generators recognize the format
- 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
switchexpressions 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:
- Always use
async/awaitinstead ofContinueWith - Handle
OperationCanceledExceptionandTaskCanceledExceptionseparately - Use
CancellationTokenfor timeout control - Use global middleware to catch unhandled async exceptions
- Log at appropriate levels
- 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:
| Scenario | Use | Status Code |
|---|---|---|
| Missing required field | Status Code | 400 Bad Request |
| Invalid data format | Status Code | 400 Bad Request |
| User not authenticated | Status Code | 401 Unauthorized |
| Resource not found | Status Code | 404 Not Found |
| Business rule violation | Status Code | 409 Conflict |
| Database connection failed | Exception → Status Code | 500 Internal Error |
| External service timeout | Exception → Status Code | 502/503 |
| Unexpected null reference | Exception → Status Code | 500 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:
- Use structured logging (Serilog, NLog)
- Include correlation IDs for request tracking
- Send to centralized logging (Application Insights, ELK Stack)
- Set up alerts for error thresholds
- Monitor error rates and patterns
- Log enough context but not sensitive data
Summary
Key Takeaways:
- Multiple approaches exist for error handling in Web API
- Combine methods for comprehensive error handling
- Use middleware for global exception handling
- Follow RFC 7807 for standard error responses
- Create custom exceptions for semantic errors
- Log appropriately with structured logging
- Handle async errors with proper async/await
- 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
Post a Comment