Exception Handling
What are Exceptions?
An exception is an error that happens while your program is running. Think of it like a roadblock - something unexpected that stops your code from working.
Error
Something went wrong
Handle
Protect your program
Fix
Prevent crashes
Common Exceptions
Here are the most common exceptions you'll encounter in C#:
| Exception | When It Happens | Example |
|---|---|---|
DivideByZeroException |
Dividing by zero | int result = 10 / 0; |
IndexOutOfRangeException |
Accessing an index outside array bounds | array[10] when array has only 5 items |
NullReferenceException |
Using an object that is null | string s = null; s.Length; |
FormatException |
Invalid format when converting | int.Parse("abc") |
FileNotFoundException |
File doesn't exist | File.ReadAllText("notfound.txt") |
ArgumentException |
Invalid argument passed | Method with invalid parameter |
Basic Try-Catch
The try-catch block is the foundation of exception handling.
You put code that might fail in the try block, and handle errors in the catch block.
// ===== BASIC TRY-CATCH STRUCTURE =====
try
{
// Code that might cause an error
int result = 10 / 0; // This will throw DivideByZeroException
Console.WriteLine(result);
}
catch (DivideByZeroException ex)
{
// This code runs if an error happens
Console.WriteLine($"Error: {ex.Message}");
}
Console.WriteLine("Program continues..."); // This still runs
// ===== REAL-WORLD EXAMPLE: Safe Division =====
int SafeDivide(int a, int b)
{
try
{
return a / b;
}
catch (DivideByZeroException)
{
Console.WriteLine("Cannot divide by zero!");
return 0;
}
}
Multiple Catch Blocks
You can have multiple catch blocks to handle different types of exceptions. The most specific exceptions should come first.
// ===== MULTIPLE CATCH BLOCKS =====
try
{
string input = null;
int[] numbers = { 1, 2, 3 };
int value = numbers[5]; // IndexOutOfRangeException
int length = input.Length; // NullReferenceException
}
catch (IndexOutOfRangeException ex)
{
// Handle index errors
Console.WriteLine($"Index error: {ex.Message}");
}
catch (NullReferenceException ex)
{
// Handle null errors
Console.WriteLine($"Null error: {ex.Message}");
}
catch (Exception ex)
{
// Handle ALL other errors (generic)
Console.WriteLine($"General error: {ex.Message}");
}
// ===== IMPORTANT: Order Matters! =====
// Put the most specific exceptions first, generic Exception last
Exception (generic) last!
The Finally Block
The finally block always runs - whether an exception occurs or not. It's perfect for cleaning up resources like closing files or database connections.
// ===== FINALLY BLOCK - Always Runs =====
FileStream file = null;
try
{
file = File.Open("data.txt", FileMode.Open);
// Process file...
Console.WriteLine("File processed");
}
catch (FileNotFoundException ex)
{
Console.WriteLine($"File not found: {ex.Message}");
}
finally
{
// This ALWAYS runs - even if there was an error!
if (file != null)
{
file.Close(); // Always close the file
Console.WriteLine("File closed");
}
}
// ===== REAL-WORLD EXAMPLE: Database Connection =====
SqlConnection connection = new SqlConnection(connectionString);
try
{
connection.Open();
// Execute database commands...
}
finally
{
if (connection.State == ConnectionState.Open)
{
connection.Close(); // Always close the connection
}
}
finally block is great for
cleaning up resources like files, database connections, and network streams.
The Using Statement (Auto-Dispose)
The using statement is a shortcut for try-finally when working with resources that need to be disposed (like files or database connections).
// ===== WITHOUT USING (Long way) =====
FileStream file = null;
try
{
file = File.Open("data.txt", FileMode.Open);
// Read file...
}
finally
{
file?.Dispose(); // Manual cleanup
}
// ===== WITH USING (Short and clean) =====
using (var file = File.Open("data.txt", FileMode.Open))
{
// Read file...
} // Automatically disposed here!
// ===== MULTIPLE USING STATEMENTS =====
using (var reader = new StreamReader("file.txt"))
using (var writer = new StreamWriter("output.txt"))
{
string content = reader.ReadToEnd();
writer.Write(content);
} // Both are automatically disposed
using when working with files,
database connections, or any object that implements IDisposable.
Throwing Exceptions
Sometimes you need to create and throw your own exceptions. This is useful when you want to stop the program when something is wrong.
// ===== THROWING AN EXCEPTION =====
public void Divide(int a, int b)
{
if (b == 0)
{
throw new DivideByZeroException("Cannot divide by zero!");
}
return a / b;
}
// ===== VALIDATING USER INPUT =====
public void SetAge(int age)
{
if (age < 0 || age > 150)
{
throw new ArgumentException(
$"Age must be between 0 and 150. You entered: {age}");
}
_age = age;
}
// ===== USING THE METHOD WITH TRY-CATCH =====
try
{
SetAge(200); // This will throw an exception
}
catch (ArgumentException ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
Creating Custom Exceptions
You can create your own exception classes to represent specific errors in your application. This makes error handling more meaningful.
// ===== CREATING A CUSTOM EXCEPTION =====
public class InvalidEmailException : Exception
{
public string Email { get; }
// Constructor with message
public InvalidEmailException(string email)
: base($"Email '{email}' is invalid")
{
Email = email;
}
// Constructor with message and inner exception
public InvalidEmailException(string email, Exception inner)
: base($"Email '{email}' is invalid", inner)
{
Email = email;
}
}
// ===== USING THE CUSTOM EXCEPTION =====
public void ValidateEmail(string email)
{
if (string.IsNullOrWhiteSpace(email))
{
throw new InvalidEmailException(email);
}
if (!email.Contains("@"))
{
throw new InvalidEmailException(email);
}
}
// ===== HANDLING THE CUSTOM EXCEPTION =====
try
{
ValidateEmail("invalid-email");
}
catch (InvalidEmailException ex)
{
Console.WriteLine($"Invalid email: {ex.Email}");
Console.WriteLine($"Error: {ex.Message}");
}
Exception Handling Best Practices
DO
- Handle specific exceptions first
- Use
usingfor disposable resources - Log exceptions for debugging
- Show user-friendly error messages
- Use
finallyfor cleanup - Validate input before using it
DON'T
- Don't catch exceptions you can't handle
- Don't use empty catch blocks
- Don't throw
Exception(be specific) - Don't hide errors - log them
- Don't use exceptions for normal flow
- Don't swallow exceptions silently
Quick Reference: Try-Catch-Finally
| Block | Purpose | Always Runs? |
|---|---|---|
| try | Contains code that might throw an exception | Yes (runs first) |
| catch | Handles the exception | Only if an exception occurs |
| finally | Cleanup code (closing files, connections) | Always (even if no exception) |
| using | Auto-dispose resources (shortcut) | Always (implicitly) |
Exercise: Safe Calculator
Task: Create a calculator that handles errors gracefully.
Instructions:
- Create a console application called "SafeCalculator"
- Ask the user to enter two numbers and an operator (+, -, *, /)
- Handle these exceptions:
- FormatException - if the user enters text instead of numbers
- DivideByZeroException - if the user tries to divide by zero
- ArgumentException - if the operator is invalid
- Use a finally block to log the operation
- Create a custom exception called
InvalidOperatorException - Keep asking until the user enters valid input
- Use
int.TryParse()ortry-catchfor number input - Use a
whileloop to keep asking - Use
switchorif-elsefor the operator - Log the operation in the finally block
Enter first number: 10 Enter operator (+, -, *, /): / Enter second number: 0 Error: Cannot divide by zero! Try again! Enter first number: 10 Enter operator (+, -, *, /): + Enter second number: 5 Result: 15 Operation logged: 10 + 5 = 15
Key Takeaway
Exception handling is essential for building robust applications:
✅ Use try-catch to handle errors gracefully
✅ Use finally for cleanup code that must run
✅ Use using for automatic resource disposal
✅ Throw exceptions when input is invalid
✅ Create custom exceptions for specific errors
✅ Always log exceptions for debugging
✅ Show user-friendly error messages