C# Exception Handling — Try/Catch/Finally, Custom Exceptions, and When Filters
In this tutorial, you will learn about C# Exception Handling. We cover key concepts, practical examples, and best practices to help you master this topic.
C# exception handling uses try/catch/finally blocks to manage runtime errors gracefully, with support for custom exception types and when filters for conditional catching.
What You'll Learn
You will master exception handling in C#: try/catch blocks for catching exceptions, finally blocks for cleanup, throwing and re-throwing exceptions, creating custom exception types, using when filters for conditional catching, and best practices for robust .NET error handling.
Why It Matters
Proper exception handling distinguishes robust applications from fragile ones. Unhandled exceptions crash applications. Caught too broadly, they hide bugs. Exception handling in C# is not just about try/catch — it is about designing error-resistant systems. Understanding the nuances of exception filters, stack traces, and cleanup patterns is essential for production-quality code.
Real-World Use
ASP.NET Core middleware catches exceptions globally to return proper HTTP error responses. File I/O operations wrap streams in try/catch to handle disk errors. Network operations catch timeouts and connection failures. Database operations handle constraint violations. Background services use try/catch to prevent crashes from stopping entire applications.
Learning Path
graph LR
A["19: Generics"] --> B["20: Exception Handling"]
B --> C["21: LINQ"]
C --> D["22: LINQ Advanced"]
D --> E["23: Delegates & Events"]
style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
style D fill:#4a90d9,stroke:#2c5f8a,color:#fff
style E fill:#4a90d9,stroke:#2c5f8a,color:#fff
Basic Try/Catch
try
{
Console.Write("Enter a number: ");
string input = Console.ReadLine();
int number = int.Parse(input);
Console.WriteLine($"You entered: {number}");
}
catch (FormatException)
{
Console.WriteLine("Invalid format! Please enter a valid number.");
}
catch (OverflowException)
{
Console.WriteLine("Number is too large or too small for an integer.");
}
Multiple Catch Blocks
try
{
string[] files = { "file1.txt", null, "file2.txt" };
foreach (var file in files)
{
string content = File.ReadAllText(file); // May throw multiple exception types
Console.WriteLine($"Read {content.Length} characters");
}
}
catch (ArgumentNullException ex) when (ex.ParamName == "path")
{
Console.WriteLine($"Null file path: {ex.Message}");
}
catch (FileNotFoundException ex)
{
Console.WriteLine($"File not found: {ex.FileName}");
}
catch (UnauthorizedAccessException ex)
{
Console.WriteLine($"Access denied: {ex.Message}");
}
catch (Exception ex) when (LogException(ex))
{
// Catch-all that logs but does not handle
throw; // Re-throw
}
static bool LogException(Exception ex)
{
Console.WriteLine($"Logged: {ex.GetType().Name}: {ex.Message}");
return false; // Does not catch, continues up
}
Finally Block
The finally block always executes, whether an exception occurs or not:
FileStream? file = null;
try
{
file = File.OpenRead("data.txt");
byte[] buffer = new byte[1024];
int bytesRead = file.Read(buffer);
Console.WriteLine($"Read {bytesRead} bytes");
}
catch (FileNotFoundException ex)
{
Console.WriteLine($"File not found: {ex.Message}");
}
finally
{
file?.Dispose(); // Always clean up
Console.WriteLine("Cleanup completed");
}
Throwing Exceptions
public class Account
{
public decimal Balance { get; private set; }
public void Withdraw(decimal amount)
{
if (amount <= 0)
throw new ArgumentException("Amount must be positive", nameof(amount));
if (amount > Balance)
throw new InvalidOperationException("Insufficient funds");
Balance -= amount;
}
}
var account = new Account();
try
{
account.Withdraw(100); // Balance is 0, throws
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"Transaction failed: {ex.Message}");
}
Re-Throwing Exceptions
try
{
DoWork();
}
catch (Exception ex)
{
// Log and re-throw (preserving stack trace)
Console.WriteLine($"Error: {ex.Message}");
throw; // Preserves original stack trace
// throw ex; // BAD: resets stack trace to here
}
Custom Exception Types
public class PaymentProcessingException : Exception
{
public string TransactionId { get; }
public decimal Amount { get; }
public PaymentProcessingException(string transactionId, decimal amount, string message)
: base(message)
{
TransactionId = transactionId;
Amount = amount;
}
public PaymentProcessingException(string transactionId, decimal amount,
string message, Exception innerException)
: base(message, innerException)
{
TransactionId = transactionId;
Amount = amount;
}
}
public class PaymentGateway
{
public void ProcessPayment(string transactionId, decimal amount)
{
try
{
// Simulate payment processing
if (amount > 10000)
throw new InvalidOperationException("Amount exceeds limit");
}
catch (Exception ex)
{
throw new PaymentProcessingException(transactionId, amount,
$"Payment failed for transaction {transactionId}", ex);
}
}
}
When Filter (Exception Filters)
The when keyword allows conditional exception handling:
try
{
Console.Write("Enter age: ");
int age = int.Parse(Console.ReadLine());
ValidateAge(age);
}
catch (FormatException ex) when (ex.Message.Contains("Input string was not in a correct format"))
{
Console.WriteLine("Please enter digits only.");
}
catch (FormatException ex) when (ex.Message.Contains("Index was out of range"))
{
Console.WriteLine("Different format exception context.");
}
catch (ArgumentException ex) when (ex.ParamName == "age")
{
Console.WriteLine($"Age validation: {ex.Message}");
}
void ValidateAge(int age)
{
if (age < 0) throw new ArgumentException("Age cannot be negative", "age");
if (age > 150) throw new ArgumentException("Age seems unrealistic", "age");
}
Global Exception Handling
// Top-level statement with global handler
AppDomain.CurrentDomain.UnhandledException += (sender, args) =>
{
var ex = (Exception)args.ExceptionObject;
Console.WriteLine($"CRITICAL: {ex.Message}");
// Log and save state before crash
};
// Task exception handler
TaskScheduler.UnobservedTaskException += (sender, args) =>
{
Console.WriteLine($"Unobserved task exception: {args.Exception.Message}");
args.SetObserved();
};
Using Statement (IDisposable)
The using statement ensures deterministic cleanup:
// Equivalent to try/finally
using (var reader = new StreamReader("file.txt"))
{
string content = reader.ReadToEnd();
Console.WriteLine(content);
} // reader.Dispose() called here
// Using declaration (C# 8+)
using var writer = new StreamWriter("output.txt");
writer.WriteLine("Hello");
// writer.Dispose() called at end of scope
Best Practices
// GOOD: Specific exception types
try { /* ... */ }
catch (FileNotFoundException) { /* handle */ }
// BAD: Catching Exception and checking type
try { /* ... */ }
catch (Exception ex) when (ex is FileNotFoundException) { /* handle */ }
// GOOD: Preserve stack trace
catch { throw; }
// BAD: Reset stack trace
catch (Exception ex) { throw ex; }
// GOOD: Use finally for cleanup
// BAD: Catch, log, swallow silently
// GOOD: Fail fast for unrecoverable errors
public void Process(string data)
{
ArgumentException.ThrowIfNullOrEmpty(data);
// ... process
}
Common Mistakes
Mistake 1: Catching Exception Too Broadly
catch (Exception ex) catches everything including StackOverflowException, OutOfMemoryException, and AccessViolationException. Catch specific exception types whenever possible.
Mistake 2: Swallowing Exceptions Silently
try { DoSomething(); }
catch { /* Do nothing - BAD! */ }
Mistake 3: Using throw ex Instead of throw
throw ex resets the stack trace to the point of the throw, losing the original error location. Use throw to preserve the original stack trace.
Mistake 4: Throwing Exception Instead of a Specific Type
Always throw the most specific exception type available: ArgumentNullException, InvalidOperationException, ArgumentException, or a custom type.
Mistake 5: Not Using Finally for Cleanup
Resources (streams, connections, handles) must be released even when exceptions occur. Use finally or using statements.
Mistake 6: Using Exceptions for Control Flow
Exceptions are for exceptional conditions, not for normal control flow. Throwing/catching exceptions is expensive. Use return values or patterns for expected conditions.
Practice Questions
- What is the purpose of the
finallyblock? - How does
whenin a catch block differ from an if statement inside the catch? - Why is
throw exconsidered harmful? - When would you create a custom exception type?
- Write a try/catch/finally block that handles file access and always closes the file.
Challenge
Create a retry mechanism that retries an operation up to 3 times with exponential backoff. The retry should catch specific exceptions (TimeoutException, HttpRequestException) and re-throw if the retry count is exhausted.
FAQ
Mini Project
Create a robust file processing system:
public class FileProcessingException : Exception
{
public string FilePath { get; }
public FileProcessingException(string filePath, string message, Exception? inner = null)
: base(message, inner) => FilePath = filePath;
}
public class FileProcessor
{
public async Task ProcessFileAsync(string filePath, int maxRetries = 3)
{
for (int attempt = 1; attempt <= maxRetries; attempt++)
{
try
{
Console.WriteLine($"Attempt {attempt}: Processing {filePath}");
await ProcessInternalAsync(filePath);
return; // Success
}
catch (IOException ex) when (attempt < maxRetries)
{
Console.WriteLine($"IO error, retrying... ({ex.Message})");
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt)));
}
catch (Exception ex)
{
throw new FileProcessingException(filePath,
$"Failed to process after {attempt} attempts", ex);
}
}
}
private async Task ProcessInternalAsync(string filePath)
{
if (!File.Exists(filePath))
throw new FileNotFoundException("File not found", filePath);
using var stream = File.OpenRead(filePath);
using var reader = new StreamReader(stream);
string? line;
int lineCount = 0;
while ((line = await reader.ReadLineAsync()) != null)
{
lineCount++;
if (string.IsNullOrWhiteSpace(line)) continue;
Console.WriteLine($" Line {lineCount}: {line.Trim()}");
}
Console.WriteLine($"Processed {lineCount} lines from {filePath}");
}
}
var processor = new FileProcessor();
try
{
// First with a file that exists
await File.WriteAllTextAsync("test.txt", "Hello\nWorld\nC# Exceptions");
await processor.ProcessFileAsync("test.txt", maxRetries: 1);
Console.WriteLine();
// Then with a file that does not exist
await processor.ProcessFileAsync("nonexistent.txt");
}
catch (FileProcessingException ex)
{
Console.WriteLine($"\nFile processing error: {ex.Message}");
Console.WriteLine($" File: {ex.FilePath}");
if (ex.InnerException != null)
Console.WriteLine($" Cause: {ex.InnerException.GetType().Name}");
}
finally
{
if (File.Exists("test.txt")) File.Delete("test.txt");
}
Expected output:
Attempt 1: Processing test.txt
Line 1: Hello
Line 2: World
Line 3: C# Exceptions
Processed 3 lines from test.txt
Attempt 1: Processing nonexistent.txt
IO error, retrying... (File not found)
File processing error: Failed to process after 3 attempts
File: nonexistent.txt
Cause: FileNotFoundException
What's Next
You have mastered exception handling in C#. The next lesson covers LINQ: querying data with Where, Select, GroupBy, OrderBy, and aggregation operations.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro