Error handling is a crucial part of building robust web APIs. Proper error handling ensures that your application can gracefully handle unexpected situations and provide meaningful responses to clients. This guide covers everything you need to know about error handling in ASP.NET Core 8 Web API, including basic concepts, middleware, custom exceptions, logging, and best practices.
Error handling in web APIs involves intercepting errors, logging them, and returning appropriate HTTP responses to the client. This helps in maintaining a stable and user-friendly API.
The most basic form of error handling is using try-catch blocks in your controller actions.
Example:
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
[HttpGet("{id}")]
public IActionResult GetProduct(int id)
{
try
{
// Simulate a database fetch
var product = new Product { Id = id, Name = "Sample Product", Price = 9.99m };
if (product == null)
{
return NotFound();
}
return Ok(product);
}
catch (Exception ex)
{
// Log the exception (not shown)
return StatusCode(500, "Internal server error");
}
}
}
A more robust approach is to use middleware for global error handling. This ensures that all unhandled exceptions are caught and processed in a consistent manner.
3.1 Custom Exception Handling Middleware Create Middleware:
public class ExceptionHandlingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ExceptionHandlingMiddleware> _logger;
public ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
_logger.LogError(ex, "An unhandled exception has occurred.");
await HandleExceptionAsync(context, ex);
}
}
private static Task HandleExceptionAsync(HttpContext context, Exception exception)
{
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
var response = new
{
message = "An unexpected error occurred.",
detail = exception.Message
};
var jsonResponse = JsonSerializer.Serialize(response);
return context.Response.WriteAsync(jsonResponse);
}
}
var builder = WebApplication.CreateBuilder(args);
// Add services to the container
builder.Services.AddControllers();
builder.Services.AddLogging();
var app = builder.Build();
// Use the custom exception handling middleware
app.UseMiddleware<ExceptionHandlingMiddleware>();
app.MapControllers();
app.Run();
Defining custom exception types helps in categorizing errors and handling them appropriately.
4.1 Define Custom Exceptions Example:
public class NotFoundException : Exception
{
public NotFoundException(string message) : base(message) { }
}
public class BadRequestException : Exception
{
public BadRequestException(string message) : base(message) { }
}
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
[HttpGet("{id}")]
public IActionResult GetProduct(int id)
{
try
{
// Simulate a database fetch
var product = new Product { Id = id, Name = "Sample Product", Price = 9.99m };
if (product == null)
{
throw new NotFoundException("Product not found");
}
return Ok(product);
}
catch (NotFoundException ex)
{
return NotFound(new { message = ex.Message });
}
catch (Exception ex)
{
// Log the exception (not shown)
return StatusCode(500, "Internal server error");
}
}
}
ASP.NET Core provides built-in support for model validation errors. You can customize how these errors are handled.
5.1 Customizing Validation Error Responses Configure API Behavior:
builder.Services.Configure<ApiBehaviorOptions>(options =>
{
options.InvalidModelStateResponseFactory = context =>
{
var errors = context.ModelState
.Where(e => e.Value.Errors.Count > 0)
.Select(e => new
{
Field = e.Key,
Errors = e.Value.Errors.Select(x => x.ErrorMessage).ToArray()
}).ToArray();
var errorResponse = new
{
message = "Validation failed",
errors = errors
};
return new BadRequestObjectResult(errorResponse);
};
});
public class Product
{
public int Id { get; set; }
[Required(ErrorMessage = "Name is required")]
[StringLength(100, ErrorMessage = "Name can't be longer than 100 characters")]
public string Name { get; set; }
[Range(0.01, 1000.00, ErrorMessage = "Price must be between 0.01 and 1000.00")]
public decimal Price { get; set; }
}
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
[HttpPost]
public IActionResult CreateProduct([FromBody] Product product)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
// Save the product to the database (not shown)
return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, product);
}
[HttpGet("{id}")]
public IActionResult GetProduct(int id)
{
// Retrieve the product from the database (not shown)
var product = new Product { Id = id, Name = "Sample Product", Price = 9.99m };
return Ok(product);
}
}
Logging errors is crucial for diagnosing and troubleshooting issues in your application.
6.1 Configure Logging Configure Logging in Program.cs:
var builder = WebApplication.CreateBuilder(args);
// Configure logging
builder.Logging.ClearProviders();
builder.Logging.AddConsole();
var app = builder.Build();
public class ExceptionHandlingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ExceptionHandlingMiddleware> _logger;
public ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
_logger.LogError(ex, "An unhandled exception has occurred.");
await HandleExceptionAsync(context, ex);
}
}
private static Task HandleExceptionAsync(HttpContext context, Exception exception)
{
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
var response = new
{
message = "An unexpected error occurred.",
detail = exception.Message
};
var jsonResponse = JsonSerializer.Serialize(response);
return context.Response.WriteAsync(jsonResponse);
}
}
Here is a comprehensive example combining global error handling, custom exceptions, validation, and logging.
Program.cs:
var builder = WebApplication.CreateBuilder(args);
// Add services to the container
builder.Services.AddControllers();
builder.Services.AddLogging();
builder.Services.Configure<ApiBehaviorOptions>(options =>
{
options.InvalidModelStateResponseFactory = context =>
{
var errors = context.ModelState
.Where(e => e.Value.Errors.Count > 0)
.Select(e => new
{
Field = e.Key,
Errors = e.Value.Errors.Select(x => x.ErrorMessage).ToArray()
}).ToArray();
var errorResponse = new
{
message = "Validation failed",
errors = errors
};
return new BadRequestObjectResult(errorResponse);
};
});
var app = builder.Build();
// Use the custom exception handling middleware
app.UseMiddleware<ExceptionHandlingMiddleware>();
app.MapControllers();
app.Run();
public class NotFoundException : Exception
{
public NotFoundException(string message) : base(message) { }
}
public class BadRequestException : Exception
{
public BadRequestException(string message) : base(message) { }
}
public class ExceptionHandlingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ExceptionHandlingMiddleware> _logger;
public ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (NotFoundException ex)
{
await HandleExceptionAsync(context, ex, HttpStatusCode.NotFound);
}
catch (BadRequestException ex)
{
await HandleExceptionAsync(context, ex, HttpStatusCode.BadRequest);
}
catch (Exception ex)
{
_logger.LogError(ex, "An unhandled exception has occurred.");
await HandleExceptionAsync(context, ex, HttpStatusCode.InternalServerError);
}
}
private static Task HandleExceptionAsync(HttpContext context, Exception exception, HttpStatusCode statusCode)
{
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)statusCode;
var response = new
{
message = exception.Message,
detail = exception is CustomException ? exception.Message : "An unexpected error occurred."
};
var jsonResponse = JsonSerializer.Serialize(response);
return context.Response.WriteAsync(jsonResponse);
}
}
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
[HttpPost]
public IActionResult CreateProduct([FromBody] Product product)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
// Simulate a scenario where product creation fails
throw new BadRequestException("Failed to create the product");
return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, product);
}
[HttpGet("{id}")]
public IActionResult GetProduct(int id)
{
// Simulate a scenario where the product is not found
throw new NotFoundException("Product not found");
// Simulate a successful product retrieval
var product = new Product { Id = id, Name = "Sample Product", Price = 9.99m };
return Ok(product);
}
}
public class Product
{
public int Id { get; set; }
[Required(ErrorMessage = "Name is required")]
[StringLength(100, ErrorMessage = "Name can't be longer than 100 characters")]
public string Name { get; set; }
[Range(0.01, 1000.00, ErrorMessage = "Price must be between 0.01 and 1000.00")]
public decimal Price { get; set; }
}
Proper error handling is essential for building robust and user-friendly web APIs. By leveraging global error handling middleware, custom exceptions, validation error handling, and logging, you can ensure that your ASP.NET Core 8 Web API handles errors gracefully and provides meaningful responses to clients. This comprehensive guide provides the tools and knowledge to implement effective error handling in your ASP.NET Core 8 Web API projects.