Skip to main content

.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.

csharp
// ❌ 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.

csharp
// ❌ 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.

csharp
// ✅ 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.

csharp
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.

csharp
// ❌ 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.

csharp
// 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:

csharp
// ❌ 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:

csharp
// 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:

csharp
// 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:

csharp
// 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:

  1. Use specific exception types instead of catching general exceptions
  2. Include meaningful information in exception messages
  3. Always clean up resources with using statements
  4. Utilize exception filters for more targeted exception handling
  5. Avoid empty catch blocks that hide errors
  6. Create custom exception classes for domain-specific errors
  7. Use Try-Parse patterns for conversions
  8. Handle exceptions properly in async code
  9. 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

Exercises

  1. Refactor Challenge: Take a piece of code that uses general exception catching and refactor it to use specific exception types.
  2. Custom Exception: Create a custom exception class for a specific domain problem in your application.
  3. Global Handler: Implement a global exception handler for a console or web application.
  4. Exception Filter: Write an exception handler that uses exception filters to handle different error conditions.
  5. Resource Cleanup: Find code that manages resources and ensure it properly cleans up resources using using statements or try-finally blocks.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)