On this page
Exception Handling
Exceptions represent runtime errors. C# uses structured exception handling with try, catch, finally, and throw.
Basic try-catch
try
{
int result = Divide(10, 0);
Console.WriteLine(result);
}
catch (DivideByZeroException ex)
{
Console.WriteLine($"Math error: {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"Unexpected: {ex.Message}");
}
finally
{
Console.WriteLine("Cleanup always runs");
}
static int Divide(int a, int b) => a / b;
Throwing Exceptions
void ValidateAge(int age)
{
if (age < 0)
throw new ArgumentOutOfRangeException(nameof(age), "Age cannot be negative");
if (age > 150)
throw new ArgumentException("Age seems unrealistic", nameof(age));
}
try
{
ValidateAge(-5);
}
catch (ArgumentOutOfRangeException ex)
{
Console.WriteLine(ex.ParamName); // age
}
Custom Exceptions
class InsufficientFundsException : Exception
{
public decimal Balance { get; }
public InsufficientFundsException(decimal balance)
: base($"Insufficient funds. Balance: {balance:C}")
{
Balance = balance;
}
}
void Withdraw(decimal amount, decimal balance)
{
if (amount > balance)
throw new InsufficientFundsException(balance);
}
Exception Filters
try
{
ProcessData(null!);
}
catch (ArgumentNullException ex) when (ex.ParamName == "data")
{
Console.WriteLine("Data was null");
}
Best Practices
- Catch specific exceptions, not bare
Exception, when possible. - Do not use exceptions for normal control flow.
- Include meaningful messages and preserve inner exceptions.
- Use
throw;to re-throw without resetting the stack trace.
catch (IOException ex)
{
Log(ex);
throw; // preserves stack trace
}
For expected failures (file not found, invalid input), consider returning Result<T> types or nullable values instead of throwing.
AggregateException in Parallel Code
try
{
Parallel.ForEach(items, item => Process(item));
}
catch (AggregateException ae)
{
foreach (var ex in ae.InnerExceptions)
Console.WriteLine(ex.Message);
}
try-finally Without catch
FileStream? stream = null;
try
{
stream = File.OpenRead("data.bin");
// process stream
}
finally
{
stream?.Dispose();
}
Prefer using or await using over manual finally blocks.
Exception Properties
catch (SqlException ex)
{
Console.WriteLine(ex.Message); // human-readable
Console.WriteLine(ex.StackTrace); // call stack
Console.WriteLine(ex.InnerException?.Message); // wrapped cause
}
Log all three in production error handlers.
Fail Fast vs Graceful Degradation
// Fail fast — configuration errors at startup
var connString = config.GetConnectionString("Default")
?? throw new InvalidOperationException("Connection string missing");
// Graceful — optional feature unavailable
try { await SendNotificationAsync(user); }
catch (NotificationException ex)
{
logger.LogWarning(ex, "Notification failed — continuing");
}
Testing Exceptions with xUnit
[Fact]
public void ValidateAge_ThrowsForNegative()
{
var ex = Assert.Throws<ArgumentOutOfRangeException>(
() => ValidateAge(-1));
Assert.Equal("age", ex.ParamName);
}