C# Exception Handling
Introduction
Exception handling is a mechanism for responding to runtime errors in a structured way. Proper exception handling makes applications more robust, maintainable, and user-friendly.
Table of Contents
- Exception Basics
- Try-Catch-Finally
- Exception Types
- Throwing Exceptions
- Custom Exceptions
- Exception Filters
- Best Practices
- Exception Handling Patterns
- Performance Considerations
- Interview Questions
Exception Basics
What is an Exception?
An exception is an object that represents an error or unexpected condition during program execution.
// Exception hierarchy
System.Object
βββ System.Exception
βββ System.SystemException
β βββ System.ArgumentException
β β βββ System.ArgumentNullException
β β βββ System.ArgumentOutOfRangeException
β βββ System.NullReferenceException
β βββ System.InvalidOperationException
β βββ System.IndexOutOfRangeException
β βββ System.DivideByZeroException
β βββ System.IO.IOException
βββ System.ApplicationException (legacy, don't use)
Exception Properties
try
{
throw new InvalidOperationException("Operation failed");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message); // "Operation failed"
Console.WriteLine(ex.GetType().Name); // "InvalidOperationException"
Console.WriteLine(ex.StackTrace); // Stack trace
Console.WriteLine(ex.Source); // Assembly name
Console.WriteLine(ex.TargetSite); // Method that threw
Console.WriteLine(ex.InnerException); // Wrapped exception
Console.WriteLine(ex.Data); // Additional data dictionary
Console.WriteLine(ex.HResult); // HRESULT code
}
Try-Catch-Finally
Basic Structure
try
{
// Code that might throw
int result = int.Parse("not a number");
}
catch (FormatException ex)
{
// Handle specific exception
Console.WriteLine($"Invalid format: {ex.Message}");
}
catch (Exception ex)
{
// Handle any other exception
Console.WriteLine($"Error: {ex.Message}");
}
finally
{
// Always executes (cleanup)
Console.WriteLine("Cleanup complete");
}
Multiple Catch Blocks
try
{
ProcessFile(path);
}
catch (FileNotFoundException ex)
{
// Most specific first
Log($"File not found: {ex.FileName}");
throw; // Re-throw for caller to handle
}
catch (IOException ex)
{
// More general
Log($"IO Error: {ex.Message}");
}
catch (UnauthorizedAccessException ex)
{
Log($"Access denied: {ex.Message}");
}
catch (Exception ex)
{
// Catch-all (should be last)
Log($"Unexpected error: {ex.Message}");
throw; // Don't swallow unexpected exceptions
}
Try-Finally Without Catch
// Ensures cleanup even if exception propagates
FileStream stream = null;
try
{
stream = File.OpenRead(path);
ProcessStream(stream);
}
finally
{
stream?.Dispose(); // Always runs
}
// Preferred: using statement (equivalent to try-finally)
using (var stream = File.OpenRead(path))
{
ProcessStream(stream);
}
// C# 8+ simplified using declaration
using var stream = File.OpenRead(path);
ProcessStream(stream);
Nested Try-Catch
try
{
try
{
// Inner operation
RiskyOperation();
}
catch (SpecificException ex)
{
// Handle specific cases locally
HandleSpecific(ex);
}
// Other operations continue
AnotherOperation();
}
catch (Exception ex)
{
// Outer handler catches unhandled exceptions
LogAndReport(ex);
}
Exception Types
Common System Exceptions
| Exception | Cause | Prevention |
|---|---|---|
NullReferenceException |
Accessing member on null | Null checks, null-conditional operators |
ArgumentNullException |
Null passed to method | Parameter validation |
ArgumentException |
Invalid argument value | Input validation |
ArgumentOutOfRangeException |
Value outside valid range | Range validation |
InvalidOperationException |
Operation invalid for current state | State checks |
IndexOutOfRangeException |
Array index out of bounds | Bounds checking |
KeyNotFoundException |
Dictionary key not found | TryGetValue |
FormatException |
Invalid string format | TryParse |
InvalidCastException |
Invalid type cast | as operator, is check |
ObjectDisposedException |
Using disposed object | Proper disposal patterns |
NotImplementedException |
Method not implemented | Development placeholder |
NotSupportedException |
Operation not supported | Design consideration |
IO Exceptions
try
{
File.ReadAllText(path);
}
catch (FileNotFoundException)
{
// File doesn't exist
}
catch (DirectoryNotFoundException)
{
// Directory doesn't exist
}
catch (UnauthorizedAccessException)
{
// No permission
}
catch (PathTooLongException)
{
// Path exceeds system limit
}
catch (IOException)
{
// General IO error (file in use, etc.)
}
Throwing Exceptions
Basic Throw
public void ProcessOrder(Order order)
{
if (order == null)
throw new ArgumentNullException(nameof(order));
if (order.Items.Count == 0)
throw new ArgumentException("Order must have items", nameof(order));
if (order.Total < 0)
throw new ArgumentOutOfRangeException(nameof(order.Total),
order.Total, "Total cannot be negative");
// Process order...
}
Throw vs Throw ex
try
{
DoSomething();
}
catch (Exception ex)
{
// β BAD: Resets stack trace
throw ex;
}
try
{
DoSomething();
}
catch (Exception ex)
{
// β
GOOD: Preserves stack trace
throw;
}
try
{
DoSomething();
}
catch (Exception ex)
{
// β
GOOD: Wrap with inner exception
throw new ServiceException("Operation failed", ex);
}
Conditional Throw Helpers
// C# 10+ ArgumentNullException helpers
public void Process(string input)
{
ArgumentNullException.ThrowIfNull(input);
ArgumentException.ThrowIfNullOrEmpty(input);
ArgumentException.ThrowIfNullOrWhiteSpace(input);
// Continue processing...
}
// Custom throw helper (reduces IL size in hot paths)
public static class ThrowHelper
{
[DoesNotReturn]
public static void ThrowArgumentNull(string paramName)
{
throw new ArgumentNullException(paramName);
}
[DoesNotReturn]
public static void ThrowInvalidOperation(string message)
{
throw new InvalidOperationException(message);
}
}
Custom Exceptions
Creating Custom Exceptions
// Full implementation following best practices
[Serializable]
public class OrderProcessingException : Exception
{
public string OrderId { get; }
public OrderErrorCode ErrorCode { get; }
public OrderProcessingException()
{
}
public OrderProcessingException(string message)
: base(message)
{
}
public OrderProcessingException(string message, Exception innerException)
: base(message, innerException)
{
}
public OrderProcessingException(string message, string orderId, OrderErrorCode errorCode)
: base(message)
{
OrderId = orderId;
ErrorCode = errorCode;
}
public OrderProcessingException(string message, string orderId,
OrderErrorCode errorCode, Exception innerException)
: base(message, innerException)
{
OrderId = orderId;
ErrorCode = errorCode;
}
// For serialization (if needed)
protected OrderProcessingException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
OrderId = info.GetString(nameof(OrderId));
ErrorCode = (OrderErrorCode)info.GetInt32(nameof(ErrorCode));
}
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
base.GetObjectData(info, context);
info.AddValue(nameof(OrderId), OrderId);
info.AddValue(nameof(ErrorCode), (int)ErrorCode);
}
}
public enum OrderErrorCode
{
Unknown,
InvalidItems,
PaymentFailed,
OutOfStock,
ShippingUnavailable
}
Using Custom Exceptions
public class OrderService
{
public void ProcessOrder(Order order)
{
try
{
ValidateOrder(order);
ProcessPayment(order);
FulfillOrder(order);
}
catch (PaymentException ex)
{
throw new OrderProcessingException(
"Payment processing failed",
order.Id,
OrderErrorCode.PaymentFailed,
ex);
}
catch (InventoryException ex)
{
throw new OrderProcessingException(
"Insufficient inventory",
order.Id,
OrderErrorCode.OutOfStock,
ex);
}
}
}
// Caller handles domain exception
try
{
orderService.ProcessOrder(order);
}
catch (OrderProcessingException ex)
{
switch (ex.ErrorCode)
{
case OrderErrorCode.PaymentFailed:
ShowPaymentRetryUI();
break;
case OrderErrorCode.OutOfStock:
ShowBackorderOption();
break;
default:
ShowGenericError(ex.Message);
break;
}
}
Exception Filters
When Clause (C# 6+)
try
{
await httpClient.GetAsync(url);
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
// Handle 404 specifically
return null;
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized)
{
// Handle 401 specifically
throw new AuthenticationException("Session expired", ex);
}
catch (HttpRequestException ex)
{
// Handle other HTTP errors
throw new ServiceException($"HTTP error: {ex.StatusCode}", ex);
}
Logging Without Catching
try
{
ProcessData();
}
catch (Exception ex) when (LogException(ex))
{
// Never executes because LogException returns false
}
static bool LogException(Exception ex)
{
Logger.Error(ex, "Exception occurred");
return false; // Don't catch, let it propagate
}
Conditional Handling
var retryCount = 0;
const int maxRetries = 3;
while (true)
{
try
{
return await CallExternalService();
}
catch (TimeoutException) when (retryCount < maxRetries)
{
retryCount++;
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, retryCount)));
// Retry
}
// On maxRetries exceeded, exception propagates
}
Best Practices
DO
// β
Catch specific exceptions
catch (FileNotFoundException ex)
// β
Use throw; to re-throw
throw;
// β
Wrap exceptions with context
throw new ServiceException("User lookup failed", ex);
// β
Validate arguments early
if (input == null) throw new ArgumentNullException(nameof(input));
// β
Use finally or using for cleanup
using var stream = File.OpenRead(path);
// β
Include relevant information in exceptions
throw new OrderException($"Order {orderId} failed") { OrderId = orderId };
// β
Log exceptions at appropriate level
catch (Exception ex)
{
_logger.LogError(ex, "Failed to process order {OrderId}", orderId);
throw;
}
DONβT
// β Empty catch block (swallows exceptions)
catch (Exception) { }
// β Catch Exception when you expect specific type
catch (Exception ex) { /* handle any error */ }
// β Use throw ex; (loses stack trace)
throw ex;
// β Use exceptions for flow control
try
{
var value = dictionary[key];
}
catch (KeyNotFoundException)
{
value = default; // Use TryGetValue instead!
}
// β Throw Exception base class
throw new Exception("Something failed");
// β Catch and ignore without logging
catch (Exception) { return null; }
// β Include sensitive information in exception messages
throw new Exception($"Login failed for password: {password}");
Exception Handling Patterns
Result Pattern (Alternative to Exceptions)
public class Result<T>
{
public bool IsSuccess { get; }
public T Value { get; }
public string Error { get; }
private Result(T value)
{
IsSuccess = true;
Value = value;
}
private Result(string error)
{
IsSuccess = false;
Error = error;
}
public static Result<T> Success(T value) => new(value);
public static Result<T> Failure(string error) => new(error);
}
// Usage
public Result<User> GetUser(int id)
{
var user = _repository.Find(id);
if (user == null)
return Result<User>.Failure($"User {id} not found");
return Result<User>.Success(user);
}
var result = GetUser(123);
if (result.IsSuccess)
ProcessUser(result.Value);
else
ShowError(result.Error);
Global Exception Handler
// ASP.NET Core middleware
public class GlobalExceptionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<GlobalExceptionMiddleware> _logger;
public GlobalExceptionMiddleware(RequestDelegate next,
ILogger<GlobalExceptionMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (ValidationException ex)
{
_logger.LogWarning(ex, "Validation error");
context.Response.StatusCode = 400;
await WriteErrorResponse(context, ex.Message);
}
catch (NotFoundException ex)
{
_logger.LogWarning(ex, "Resource not found");
context.Response.StatusCode = 404;
await WriteErrorResponse(context, ex.Message);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception");
context.Response.StatusCode = 500;
await WriteErrorResponse(context, "An error occurred");
}
}
}
Retry Pattern with Polly
using Polly;
using Polly.Retry;
// Define retry policy
AsyncRetryPolicy retryPolicy = Policy
.Handle<HttpRequestException>()
.Or<TimeoutException>()
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)),
onRetry: (exception, timeSpan, attempt, context) =>
{
_logger.LogWarning(exception,
"Retry {Attempt} after {Delay}s", attempt, timeSpan.TotalSeconds);
});
// Use policy
var result = await retryPolicy.ExecuteAsync(async () =>
{
return await httpClient.GetStringAsync(url);
});
Circuit Breaker Pattern
using Polly.CircuitBreaker;
AsyncCircuitBreakerPolicy circuitBreaker = Policy
.Handle<HttpRequestException>()
.CircuitBreakerAsync(
exceptionsAllowedBeforeBreaking: 3,
durationOfBreak: TimeSpan.FromSeconds(30),
onBreak: (ex, duration) =>
{
_logger.LogWarning("Circuit breaker opened for {Duration}s", duration.TotalSeconds);
},
onReset: () =>
{
_logger.LogInformation("Circuit breaker reset");
});
try
{
await circuitBreaker.ExecuteAsync(async () =>
{
return await httpClient.GetStringAsync(url);
});
}
catch (BrokenCircuitException)
{
// Service unavailable, use fallback
return GetCachedData();
}
Performance Considerations
Exception Cost
// Exceptions are expensive when thrown!
// Creating exception object: ~100 CPU cycles
// Throwing and catching: ~10,000-50,000 CPU cycles
// β BAD: Using exceptions for expected cases
public int ParseOrDefault(string input, int defaultValue)
{
try
{
return int.Parse(input); // Throws on invalid input
}
catch (FormatException)
{
return defaultValue;
}
}
// β
GOOD: Avoid exceptions for expected cases
public int ParseOrDefault(string input, int defaultValue)
{
return int.TryParse(input, out int result) ? result : defaultValue;
}
Try Pattern Methods
// Framework types often provide Try methods
bool success = int.TryParse("123", out int number);
bool found = dictionary.TryGetValue(key, out var value);
bool valid = Uri.TryCreate(url, UriKind.Absolute, out var uri);
// Implement Try pattern in your own code
public class UserService
{
public bool TryGetUser(int id, out User user)
{
user = _repository.Find(id);
return user != null;
}
// Or with nullable reference types (C# 8+)
public User? TryGetUser(int id)
{
return _repository.Find(id);
}
}
Benchmark: Exception vs TryParse
// Results (typical):
// TryParse: ~15 nanoseconds
// Parse with catch: ~50,000 nanoseconds (when exception thrown)
[Benchmark]
public int ParseWithTry()
{
return int.TryParse("invalid", out int result) ? result : 0;
}
[Benchmark]
public int ParseWithCatch()
{
try
{
return int.Parse("invalid");
}
catch
{
return 0;
}
}
Interview Questions
1. Whatβs the difference between throw and throw ex?
Answer:
throw;re-throws the current exception preserving the original stack trace.throw ex;throws the exception but resets the stack trace to the current location, losing information about where the exception originated.
Always use throw; when re-throwing to preserve debugging information.
2. When should you create custom exceptions?
Answer: Create custom exceptions when:
- Built-in exceptions donβt adequately describe the error
- You need to include domain-specific information (OrderId, ErrorCode, etc.)
- Callers need to handle your errors differently from system errors
- You want to abstract implementation details from callers
Donβt create custom exceptions for every error - use built-in exceptions when they fit.
3. Should you catch Exception in your code?
Answer: Generally avoid catching Exception directly. Catch specific exceptions you can handle meaningfully. However, there are valid cases:
- Global exception handlers for logging/reporting
- Fire-and-forget background tasks
- Top-level event handlers
If you catch Exception, either re-throw with throw; or log and fail gracefully. Never silently swallow all exceptions.
4. What is the purpose of the finally block?
Answer: The finally block executes whether or not an exception occurs, making it ideal for cleanup code (closing connections, releasing resources). It runs:
- After try block if no exception
- After catch block if exception was caught
- Before exception propagates if not caught
Modern C# prefers using statements which are syntactic sugar for try-finally.
5. How do exception filters (when clause) work?
Answer: Exception filters (C# 6+) allow conditional catching based on exception properties or external state. The filter runs before entering the catch block, and if it returns false, the exception continues propagating.
Benefits:
- Catch specific conditions without catching/re-throwing
- Stack not unwound when filter runs (better debugging)
- Can inspect exception without catching it (logging pattern)
catch (HttpException ex) when (ex.StatusCode == 404)
{
// Only catches 404 errors
}
6. Why are exceptions expensive?
Answer: Exceptions are expensive because:
- Runtime must capture stack trace (walks entire call stack)
- Searches for catch handlers up the call stack
- Executes finally blocks
- May trigger first-chance exception handlers
- Creates exception object with metadata
This makes exceptions unsuitable for control flow. Use Try* patterns for expected failures and reserve exceptions for truly exceptional conditions.