C# Best Practices: Exception Handling Done Right


Introduction
Exception handling is a fundamental part of writing robust software. It allows your program to respond gracefully when unexpected errors occur. However, many developers unintentionally write exception handling code that hides problems, making it harder to diagnose issues in production.
This article will explain common pitfalls in exception handling and show how to handle exceptions correctly in C# with clear, practical examples. By following these best practices, you can improve your application's reliability, maintainability, and debuggability.
Why Exception Handling Matters
When an error occurs, if it’s not handled properly, your application might crash or behave unpredictably. On the other hand, if exceptions are handled poorly — for example, caught but ignored — bugs remain hidden and cause silent failures that are difficult to trace.
Proper exception handling strikes a balance:
It catches exceptions to prevent crashes or unexpected termination.
It logs useful information so developers can understand and fix issues.
It preserves the original error details to aid debugging.
It avoids masking the real problem by swallowing or ignoring exceptions.
Common Mistakes in Exception Handling
Before we look at best practices, let’s understand some common bad patterns.
1. Swallowing Exceptions Silently
public void SaveData(Data data)
{
try
{
// Attempt to save data
}
catch (Exception)
{
// Do nothing - exception swallowed silently
}
}
Why it’s bad: The exception is caught but ignored. No error information is logged or propagated. If saving fails, you never know, and the program continues as if everything is fine.
2. Logging Vague or Unhelpful Messages
public void ProcessRequest(Request request)
{
try
{
// Process request
}
catch (Exception ex)
{
Logger.Log("Something went wrong"); // Vague message, no exception details
}
}
Why it’s bad: Logging only “Something went wrong” doesn’t help in understanding what exactly failed. You lose exception details like message, stack trace, or context.
3. Using throw ex;
Instead of throw;
public void Calculate()
{
try
{
// Some calculation
}
catch (Exception ex)
{
// Rethrowing exception but resetting stack trace
throw ex;
}
}
Why it’s bad: Using throw ex;
resets the stack trace to this line, losing the original error location. This makes debugging harder.
4. Catching General Exceptions Without Thought
try
{
// Some operation
}
catch (Exception)
{
// Catching all exceptions without specifying or handling them properly
}
Why it’s bad: Catching all exceptions blindly can hide serious issues like out-of-memory errors, stack overflows, or programming bugs that should actually crash the program or be handled differently.
Best Practices for Exception Handling in C
Let’s now cover how to avoid the pitfalls and handle exceptions effectively.
1. Do Not Swallow Exceptions Silently
Always take some action when catching exceptions, even if it’s just logging or rethrowing.
Good example:
public void UpdateRecord(Record record)
{
try
{
// Update logic
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to update record with ID {RecordId}", record.Id);
throw; // Rethrow to let calling code handle it if needed
}
}
2. Log Meaningful Context with Exceptions
Include relevant information to help diagnose issues. Context might include identifiers, input data, or operation names.
Example:
public void ProcessPayment(Payment payment)
{
try
{
// Payment processing code
}
catch (Exception ex)
{
_logger.LogError(ex, "Payment processing failed for PaymentId {PaymentId} and UserId {UserId}", payment.Id, payment.UserId);
throw;
}
}
3. Preserve the Original Stack Trace
When rethrowing exceptions, always use throw;
rather than throw ex;
to preserve the original stack trace.
Correct way:
catch (Exception)
{
// Some logging
throw; // preserves stack trace
}
Incorrect way:
catch (Exception ex)
{
throw ex; // resets stack trace, avoid this
}
4. Catch Specific Exceptions Whenever Possible
Catch only exceptions you expect and can handle, rather than catching all exceptions.
Example:
try
{
// Code that might throw specific exceptions
}
catch (FileNotFoundException ex)
{
_logger.LogWarning(ex, "File not found");
// Handle missing file scenario
}
catch (IOException ex)
{
_logger.LogError(ex, "I/O error occurred");
throw;
}
This way, you can handle known exceptions gracefully and let unknown exceptions bubble up.
5. Use Custom Exceptions with Domain-Specific Information
Define your own exception types when necessary to convey meaningful application-specific error details.
Example:
public class PaymentDeclinedException : Exception
{
public int PaymentId { get; }
public PaymentDeclinedException(int paymentId, string message) : base(message)
{
PaymentId = paymentId;
}
}
Throw and catch these to provide richer error handling:
try
{
// Process payment
}
catch (PaymentDeclinedException ex)
{
_logger.LogWarning("Payment {PaymentId} was declined: {Message}", ex.PaymentId, ex.Message);
// Take appropriate action
}
6. Avoid Using Exceptions for Control Flow
Exceptions should signal unexpected errors, not be used for normal program flow or logic.
Complete Example: Proper Exception Handling in Action
public class OrderProcessor
{
private readonly ILogger<OrderProcessor> _logger;
private readonly IOrderRepository _orderRepository;
public OrderProcessor(ILogger<OrderProcessor> logger, IOrderRepository orderRepository)
{
_logger = logger;
_orderRepository = orderRepository;
}
public void ProcessOrder(int orderId)
{
try
{
var order = _orderRepository.GetOrderById(orderId);
if (order == null)
{
throw new ArgumentException($"Order with ID {orderId} does not exist.");
}
// Business logic to process order
_orderRepository.UpdateOrder(order);
}
catch (ArgumentException ex)
{
_logger.LogWarning(ex, "Invalid input for order processing");
// Possibly return a friendly message or handle validation issues
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error while processing order {OrderId}", orderId);
throw; // Let the caller decide how to handle unexpected exceptions
}
}
}
Summary
Good exception handling in C# requires:
Never swallowing exceptions without action.
Logging exceptions with meaningful and relevant context.
Preserving the original stack trace by using
throw;
when rethrowing.Catching specific exceptions where possible.
Using custom exceptions for domain clarity.
Avoiding catch-all blocks unless truly necessary and with appropriate handling.
Following these guidelines helps you write code that is easier to maintain, debug, and test, leading to more reliable software.
Your Thoughts
What are some exception handling mistakes you’ve encountered? How do you handle logging and rethrowing in your projects? Share your experiences and questions below.
Subscribe to my newsletter
Read articles from Ashok K directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
