Introduction: Making Software Bulletproof

Welcome to Exception Handling! Imagine you are driving a car and suddenly hit a patch of ice. A car equipped with anti-lock brakes and stability control handles the unexpected hazard smoothly and keeps you safe. A car without these features might spin completely out of control.

In software development, exceptions are those sudden patches of ice. They are unexpected events or errors that occur while your program is running. Without proper exception handling, your program will crash abruptly, leaving users confused and frustrated. With exception handling, your application stays robust, secure, and user-friendly by managing problems gracefully.

Don't worry if this seems tricky at first—by the end of these notes, you will understand exactly how to catch, handle, and manage errors like a seasoned programmer!

1. Understanding Errors and Exceptions

Before diving into handling code, let's understand the different types of problems a programmer might face:

Syntax Errors: Mistakes in the grammar of the programming language (such as a missing semicolon or a misspelled keyword). The compiler catches these before the program even runs.
Logic Errors: The code runs without crashing, but produces the wrong result (for example, calculating \( \text{Area} = \text{length} + \text{width} \) instead of \( \text{length} \times \text{width} \)).
Runtime Errors (Exceptions): Errors that happen while the application is executing. The syntax is fine, but the program is asked to do something impossible at that moment—such as reading a file that has been deleted, dividing a number by zero, or parsing letters into an integer.

What is an Exception?

An exception is an object created (or "thrown") by the runtime environment when an abnormal condition or error occurs during execution. It packages information about the problem, including the error type and where in the code it happened.

Key Takeaway: Syntax errors prevent code from compiling; runtime exceptions occur during execution when an illegal operation is attempted. Exception handling is designed specifically for runtime errors.

2. The Structured Exception Handling Mechanism: try, catch, and finally

In C# and Object-Oriented Development, structured exception handling is built around four primary keywords: try, catch, finally, and throw.

The try Block

The try block encloses the code that might potentially cause an error. Think of this as the "danger zone" where risky operations (like user input, file handling, or network communication) take place.

Example concept:
try
{
    // Code that might fail (e.g., converting text to an int)
}

The catch Block

The catch block acts as the "safety net." If an exception occurs inside the corresponding try block, the runtime stops regular execution and jumps straight into the catch block to handle the error gracefully.

Example concept:
catch (FormatException ex)
{
    // Inform the user: "Please enter a valid number."
}

The finally Block

The finally block contains code that always runs, regardless of whether an exception occurred or was handled. It is typically used for housekeeping and cleaning up resources, such as closing file streams or database connections.

Did you know? Even if your try or catch block contains a return statement, the finally block will still execute before the method finishes!

Putting It All Together

try
{
    // 1. Attempt dangerous operation
}
catch (Exception ex)
{
    // 2. Run this ONLY if an error occurs
}
finally
{
    // 3. ALWAYS run this, error or no error
}

Key Takeaway: try guards risky code, catch handles specific runtime problems, and finally ensures essential cleanup code executes no matter what.

3. Common .NET Exception Types

In C#, all exceptions inherit from the base class System.Exception. Here are the common built-in exception classes you must recognise for AS level:

FormatException: Occurs when the format of an argument is invalid. For example, trying to parse the string "hello" into an integer using \( \text{int.Parse()} \).
DivideByZeroException: Occurs when an attempt is made to divide an integer value by zero, such as \( 10 / 0 \).
IndexOutOfRangeException: Thrown when attempting to access an element of an array or collection using an index that is outside its valid bounds (e.g., accessing index 5 in an array of length 3).
NullReferenceException: Occurs when attempting to call a method or access a property on an object variable that is currently set to null (points to nothing in memory).
OverflowException: Thrown when an arithmetic operation produces a result outside the range of the data type in a checked context.
FileNotFoundException / IOException: Occurs when an input/output operation fails, such as trying to read a file from a disk path that does not exist.

Memory Aid: "FIND-NO"
F - FormatException
I - IndexOutOfRangeException
N - NullReferenceException
D - DivideByZeroException
O - OverflowException

Key Takeaway: Specific exceptions describe specific runtime failures. Identifying the right exception type allows your program to provide clear and actionable feedback to the user.

4. Multiple catch Blocks and Exception Hierarchy

A single try block can have multiple catch blocks. This allows you to respond differently depending on the specific error that occurred.

The Specific-to-General Rule

Because exceptions exist in an inheritance hierarchy, a generic catch block (like catching Exception) will catch every exception. Therefore, you must place your catch blocks in order from most specific to most general.

If you put a generic catch (Exception ex) at the top, it will intercept all errors, making any specific catch blocks below it unreachable and causing a compiler error!

Step-by-Step Example

try
{
    int number = int.Parse(userInput);
    int result = 100 / number;
}
catch (FormatException ex)
{
    // Catches non-numeric text input
    Console.WriteLine("Error: You must type digits only.");
}
catch (DivideByZeroException ex)
{
    // Catches zero input
    Console.WriteLine("Error: Division by zero is not allowed.");
}
catch (Exception ex)
{
    // Catches any other unexpected error
    Console.WriteLine("An unexpected error occurred: " + ex.Message);
}

Key Takeaway: Always arrange multiple catch blocks from the most specific exception class down to the base Exception class.

5. Raising Exceptions: The throw Keyword

Sometimes your program needs to deliberately generate an exception when business rules or validation checks are broken. You do this using the throw keyword.

Why throw an exception?

Imagine a method that sets an employee's hourly wage. A negative wage doesn't break C# syntax or computer math, but it breaks real-world business logic. If a negative number is passed in, you can deliberately throw an ArgumentOutOfRangeException.

Example concept:
public void SetWage(double wage)
{
    if (wage < 0)
    {
        throw new ArgumentOutOfRangeException("Wage cannot be negative.");
    }
    this.hourlyWage = wage;
}

Re-throwing Exceptions

Inside a catch block, you can also use throw; on its own to pass the caught exception up to the calling method after logging or partially handling it.

Key Takeaway: Use throw to signal that an invalid state or invalid data has entered your object methods.

6. Common Pitfalls to Avoid

Swallowing Exceptions (Empty Catch Blocks): Writing a catch block with nothing inside it hides bugs and makes troubleshooting nearly impossible. Always log the error or inform the user.
Using Exceptions for Normal Flow Control: Exceptions are computationally expensive. Do not use a try-catch block where a simple if statement could check the condition (for example, checking if a collection is empty before accessing it).
Wrong Catch Order: Placing a generic catch (Exception) above specific catches causes compilation failure.
Forgetting Resource Cleanup: Always use a finally block (or a C# using statement) when working with external files and database streams to prevent file-locking issues.

Quick Review Summary

Exceptions are runtime errors that halt program execution if unhandled.
try: Contains the code that might trigger an error.
catch: Handles specific exception types and prevents program crashes.
finally: Executes cleanup code guaranteed to run whether an error happened or not.
throw: Explicitly triggers an exception when invalid parameters or states occur.
Ordering: Always arrange catch blocks from most specific to least specific.