Exception Handling

Intermediate 25 min read Lesson 9 of 13

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

Think of it like this: When you drive a car, you wear a seatbelt. It doesn't prevent accidents, but it protects you when they happen. Exception handling is your program's seatbelt!

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;
    }
}
Remember: Always put code that might fail in the try block.

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
Important: The order of catch blocks matters. Put the most specific exceptions first, and 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
    }
}
Pro Tip: The 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
Pro Tip: Always use 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}");
}
When to throw: Throw exceptions when input is invalid or when the program cannot continue safely.

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}");
}
When to create custom exceptions: When you need to represent application-specific errors that are meaningful to your users.

Exception Handling Best Practices

DO
  • Handle specific exceptions first
  • Use using for disposable resources
  • Log exceptions for debugging
  • Show user-friendly error messages
  • Use finally for 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:
  1. Create a console application called "SafeCalculator"
  2. Ask the user to enter two numbers and an operator (+, -, *, /)
  3. 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
  4. Use a finally block to log the operation
  5. Create a custom exception called InvalidOperatorException
  6. Keep asking until the user enters valid input
Hints:
  • Use int.TryParse() or try-catch for number input
  • Use a while loop to keep asking
  • Use switch or if-else for the operator
  • Log the operation in the finally block
Expected Output:
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

Test Your Knowledge - Take Quiz