C# Exceptions

Exceptions are events that occur during program execution that disrupt the normal flow of instructions. C# provides a structured exception-handling mechanism that helps you manage these runtime errors so that your application can handle them gracefully instead of crashing.

When an exception occurs, the program creates an object that contains information about the error. This object is called an exception object and is derived from the System.Exception class. If an exception is not handled, the program terminates and displays an error message.

Exception Class Hierarchy

All exceptions in .NET are derived from the System.Exception class. This forms a hierarchy of exception types, allowing for more specific error handling. Common base exceptions include:

  • System.SystemException: Base class for most system-level exceptions.
  • System.ApplicationException: Base class for exceptions thrown by applications.

More specific exceptions, such as ArgumentNullException, DivideByZeroException, FileNotFoundException, and InvalidOperationException, derive from these base classes, providing detailed information about the error condition.

Handling Exceptions with try-catch

The try and catch blocks are used to handle exceptions. The code that might throw an exception is placed inside the try block. If an exception occurs within the try block, the execution is transferred to the matching catch block.

try
{
    // Code that might throw an exception
    int result = Divide(10, 0);
    Console.WriteLine($"Result: {result}");
}
catch (DivideByZeroException ex)
{
    Console.WriteLine($"An error occurred: {ex.Message}");
}
catch (Exception ex) // Catches any other exceptions
{
    Console.WriteLine($"An unexpected error occurred: {ex.GetType().Name} - {ex.Message}");
}

public int Divide(int numerator, int denominator)
{
    if (denominator == 0)
    {
        throw new DivideByZeroException("Cannot divide by zero.");
    }
    return numerator / denominator;
}

This example demonstrates how to catch a specific exception (DivideByZeroException) and a general exception.

Try it Live

The throw Statement

You can explicitly throw an exception using the throw statement. This is useful when you detect an error condition in your code that prevents it from proceeding normally.

public void ProcessData(string data)
{
    if (string.IsNullOrEmpty(data))
    {
        throw new ArgumentNullException(nameof(data), "Input data cannot be null or empty.");
    }
    // ... process data ...
    Console.WriteLine("Data processed successfully.");
}

The finally Block

The finally block contains code that is executed regardless of whether an exception is thrown or caught. This is typically used for cleanup operations, such as releasing resources.

StreamReader reader = null;
try
{
    reader = new StreamReader("my_file.txt");
    string line = reader.ReadLine();
    Console.WriteLine(line);
}
catch (FileNotFoundException)
{
    Console.WriteLine("The file was not found.");
}
finally
{
    if (reader != null)
    {
        reader.Dispose(); // Ensure the stream is closed and resources are released
        Console.WriteLine("File stream closed.");
    }
}

Exception Filters

Exception filters allow you to specify a condition that must be true for a catch block to execute. This provides more fine-grained control over exception handling.

try
{
    // ... code that might throw an exception ...
}
catch (InvalidOperationException ex) when (ex.Message.Contains("specific error"))
{
    // Handle specific error message
    Console.WriteLine($"Handling specific operation error: {ex.Message}");
}
catch (InvalidOperationException ex)
{
    // Handle any other InvalidOperationException
    Console.WriteLine($"Handling general operation error: {ex.Message}");
}

Creating Custom Exceptions

You can create your own exception types by deriving from System.Exception or one of its derived classes. This allows you to define specific error types for your application.

public class MyCustomException : Exception
{
    public MyCustomException() { }

    public MyCustomException(string message) : base(message) { }

    public MyCustomException(string message, Exception innerException) : base(message, innerException) { }
}

// Usage:
try
{
    throw new MyCustomException("Something went wrong in my custom logic.");
}
catch (MyCustomException ex)
{
    Console.WriteLine($"Caught custom exception: {ex.Message}");
}

Defining custom exceptions makes your error handling more descriptive and maintainable.