.NET Best Practices for Exception Handling
Exception handling is a critical aspect of writing robust .NET applications. Following established best practices ensures your application can gracefully handle errors, provide meaningful information to users and developers, and maintain stability even when things go wrong.
Introduction
Even the most carefully written code will encounter unexpected conditions. The difference between good applications and great ones is how they handle these situations. In this guide, we'll explore best practices for exception handling in .NET that will help you write more resilient and maintainable code.
When to Use Exceptions
Exceptions should be used for exceptional conditions - not for normal flow control. Understanding when to throw and catch exceptions is fundamental to good exception handling.
Do:
- Use exceptions for truly exceptional conditions that prevent normal program execution
- Throw exceptions when a method cannot complete its defined functionality
Don't:
- Use exceptions for expected conditions (like empty search results)
- Use exceptions for control flow (like loop termination)
Exception Handling Best Practices
1. Use Specific Exception Types
Always catch specific exceptions rather than catching the base Exception
class. This allows you to handle different error conditions appropriately.
// ❌ Avoid catching general exceptions
try
{
// Some operation that might throw
File.ReadAllText("data.txt");
}
catch (Exception ex)
{
Console.WriteLine("An error occurred.");
}
// ✅ Catch specific exceptions
try
{
// Some operation that might throw
File.ReadAllText("data.txt");
}
catch (FileNotFoundException ex)
{
Console.WriteLine("The file could not be found.");
}
catch (UnauthorizedAccessException ex)
{
Console.WriteLine("You don't have permission to access this file.");
}
2. Include Meaningful Exception Information
When throwing exceptions, provide information that helps diagnose the issue.
// ❌ Poor exception with minimal information
public void ProcessOrder(Order order)
{
if (order == null)
{
throw new ArgumentNullException();
}
// Process order
}
// ✅ Better exception with helpful information
public void ProcessOrder(Order order)
{
if (order == null)
{
throw new ArgumentNullException(nameof(order),
"Cannot process null order. Please provide a valid order object.");
}
// Process order
}
3. Clean Up Resources with Using Statements
Always use using
statements or try-finally
blocks to ensure resources are properly disposed, even when exceptions occur.
// ✅ Using statement ensures file is closed even if an exception occurs
public string ReadFileContent(string path)
{
using (StreamReader reader = new StreamReader(path))
{
return reader.ReadToEnd();
}
}
// ✅ In C# 8.0 and later, you can use the simpler syntax:
public string ReadFileContentModern(string path)
{
using var reader = new StreamReader(path);
return reader.ReadToEnd();
}
4. Use Exception Filters (When and Where Clauses)
In C#, you can use exception filters to catch exceptions only under specific conditions.
try
{
// Code that might throw
ProcessData(input);
}
catch (Exception ex) when (ex.Message.Contains("timeout"))
{
// Handle timeout-related exceptions
Console.WriteLine("Operation timed out. Please try again later.");
}
catch (Exception ex) when (IsNetworkRelated(ex))
{
// Handle network-related exceptions
Console.WriteLine("Network error. Please check your connection.");
}
5. Avoid Empty Catch Blocks
Empty catch blocks suppress errors without handling them, making debugging difficult.
// ❌ Avoid empty catch blocks
try
{
riskyOperation();
}
catch (Exception)
{
// Nothing here - BAD!
}
// ✅ At minimum, log the exception
try
{
riskyOperation();
}
catch (Exception ex)
{
logger.LogError(ex, "Error during risky operation");
// Consider whether to rethrow or handle the error
}
6. Create Custom Exception Classes When Appropriate
For domain-specific errors, create custom exception classes that extend Exception
.
// Custom exception for order-related errors
public class OrderProcessingException : Exception
{
public Order FailedOrder { get; }
public OrderProcessingException(Order order, string message, Exception innerException = null)
: base(message, innerException)
{
FailedOrder = order;
}
}
// Usage example
public void ProcessOrder(Order order)
{
try
{
// Process order
ValidateOrder(order);
ChargePayment(order);
ShipOrder(order);
}
catch (PaymentException ex)
{
throw new OrderProcessingException(order, "Payment failed", ex);
}
catch (InventoryException ex)
{
throw new OrderProcessingException(order, "Inventory check failed", ex);
}
}
7. Use Try-Parse Pattern When Converting
For conversions that might fail, use TryParse methods instead of throwing exceptions:
// ❌ Using exceptions for expected conversion failures
public int ParseUserAge(string input)
{
try
{
return int.Parse(input);
}
catch (FormatException)
{
return 0; // Default value
}
}
// ✅ Using TryParse for better performance
public int ParseUserAge(string input)
{
if (int.TryParse(input, out int age))
{
return age;
}
return 0; // Default value
}
8. Use Task-based Exception Handling for Async Code
When working with async methods, properly handle task-based exceptions:
// Handle exceptions in async methods
public async Task ProcessFileAsync(string filePath)
{
try
{
string content = await File.ReadAllTextAsync(filePath);
await ProcessContentAsync(content);
}
catch (FileNotFoundException ex)
{
logger.LogError(ex, "File not found: {FilePath}", filePath);
await NotifyUserOfMissingFileAsync(filePath);
}
catch (Exception ex)
{
logger.LogError(ex, "Error processing file: {FilePath}", filePath);
throw; // Rethrow if you can't handle it here
}
}
Real-World Example: Web API Exception Handling
Here's a more comprehensive example showing exception handling in a .NET Web API:
// Controller with proper exception handling
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
private readonly IOrderService _orderService;
private readonly ILogger<OrdersController> _logger;
public OrdersController(IOrderService orderService, ILogger<OrdersController> logger)
{
_orderService = orderService;
_logger = logger;
}
[HttpPost]
public async Task<IActionResult> CreateOrder(OrderRequest request)
{
try
{
// Validate request
if (request == null || string.IsNullOrEmpty(request.CustomerId))
{
return BadRequest("Invalid order request");
}
var order = await _orderService.CreateOrderAsync(request);
return CreatedAtAction(nameof(GetOrder), new { id = order.Id }, order);
}
catch (CustomerNotFoundException ex)
{
_logger.LogWarning(ex, "Customer not found when creating order");
return NotFound(new { message = $"Customer not found: {ex.Message}" });
}
catch (ProductUnavailableException ex)
{
_logger.LogWarning(ex, "Product unavailable when creating order");
return Conflict(new { message = $"Product unavailable: {ex.Message}" });
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error creating order");
return StatusCode(500, "An unexpected error occurred while processing your request");
}
}
// Other controller methods...
}
Global Exception Handling with Middleware
For web applications, implement global exception handling middleware to catch unhandled exceptions:
// Custom exception handling 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, "Unhandled exception");
await HandleExceptionAsync(context, ex);
}
}
private static Task HandleExceptionAsync(HttpContext context, Exception exception)
{
context.Response.ContentType = "application/json";
(int statusCode, string message) = exception switch
{
ArgumentException _ => (400, "Invalid arguments provided"),
UnauthorizedAccessException _ => (401, "Unauthorized access"),
NotFoundException _ => (404, "Requested resource not found"),
_ => (500, "An unexpected error occurred")
};
context.Response.StatusCode = statusCode;
return context.Response.WriteAsync(new ErrorResponse
{
StatusCode = statusCode,
Message = message,
#if DEBUG
Detail = exception.ToString()
#endif
}.ToString());
}
private class ErrorResponse
{
public int StatusCode { get; set; }
public string Message { get; set; }
public string Detail { get; set; }
public override string ToString()
{
return JsonSerializer.Serialize(this);
}
}
}
// Register in Startup.cs
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// Add custom exception handling middleware
app.UseMiddleware<ExceptionHandlingMiddleware>();
// Other middleware configuration
}
Summary
Proper exception handling is essential for building robust .NET applications. By following these best practices, you'll create more reliable, maintainable, and user-friendly software:
- Use specific exception types instead of catching general exceptions
- Include meaningful information in exception messages
- Always clean up resources with
using
statements - Utilize exception filters for more targeted exception handling
- Avoid empty catch blocks that hide errors
- Create custom exception classes for domain-specific errors
- Use Try-Parse patterns for conversions
- Handle exceptions properly in async code
- Implement global exception handling for web applications
Remember that good exception handling is about finding the right balance: catch exceptions you can handle meaningfully, while letting others propagate to central handling mechanisms. The goal is to maintain application stability while providing useful information for both users and developers.
Additional Resources
- Microsoft Docs: Exception Handling
- Microsoft Learn: Best practices for exceptions
- .NET Exception Handling Guidelines
Exercises
- Refactor Challenge: Take a piece of code that uses general exception catching and refactor it to use specific exception types.
- Custom Exception: Create a custom exception class for a specific domain problem in your application.
- Global Handler: Implement a global exception handler for a console or web application.
- Exception Filter: Write an exception handler that uses exception filters to handle different error conditions.
- Resource Cleanup: Find code that manages resources and ensure it properly cleans up resources using
using
statements ortry-finally
blocks.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)