A Comprehensive Guide To Exception Handling in C# .NET
Published: 2024-05-18 11:14:23

## **What is an exception?**

An exception is an occurrence during the execution of a program that disrupts it normal flow.

In the world of computers, when you tell a program to do something, sometimes it runs into a problem. An `exception` is the way the program says, "Oops! I can't do what you asked because there's a problem."

As you can see in the picture bellow the debugger which is a powerful tool that allows developers to inspect the behavior of their code during execution, making it easier to identify and fix issues such as bugs and exceptions.
In debug mode, when an exception occurs and is not caught by a **`catch`** block within the same method, the debugger typically interrupts the program execution and highlights the line of code where the exception occurred.

![VS2022 Debugger](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wkzjns9uh1ydygrezg0p.png)
## **Why handling exception?**

Let's think about riding your bike. When you're on your bike, you wear a helmet, right? That helmet is there just in case you fall, it's there to protect your head from getting hurt.

![bike helmet](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ljy6glyg2wec96oi54in.gif)
Now, in the world of computers, sometimes things go wrong when a program is trying to do its job, just like there might be a pothole or a branch that could make you fall off your bike.

Handling exceptions is like wearing your helmet when riding your bike. It is a way for the program to say, "Whoops, something went wrong here, but don't worry buddy, I know what to do!"
Just like your helmet keeps you safe, handling exceptions keeps the program safe so it doesn't crash and lose all the work it was doing. 

So by handling exceptions, we can provide meaningful error messages to the user and **prevent our** programs from terminating unexpectedly. It also allows us to implement recovery scenarios and ensures that the code following the faulty one does not get affected by an error.

## **How exception are handled**

In C# .NET, exceptions are handled using a combination of the `try`, `catch`, and `finally` keywords. 

- `try` contains the section of code that might potentially throw an exception. 

- `catch` blocks are used to handle any exceptions that occur within the corresponding `try` block. 

- The `finally` block, if present, is executed regardless of whether an exception is thrown or not. This is usually used for cleanup code that must be run regardless of whether an operation succeeds or fails.

In this example **`MethodToHandleException`** attempts to divide 10 by 0 using the **`Divide`** method. If a **`DivideByZeroException`** occurs, it prints an error message, otherwise, it catches any other exceptions and prints a generic error message.

The method ensures cleanup by printing "Cleaning up..." regardless of whether an exception occurs. 

```csharp
  public void HandleDivideByZeroException()
    {
        try
        {
            // Code that might throw an exception
            Divide(10, 0); // Attempting to divide by zero
        }
        catch (DivideByZeroException ex)
        {
            // Handle divide by zero exception
            Console.WriteLine("Error: " + ex.Message);
            // Rethrow the exception without losing the original stack trace
            throw;
        }
        catch (Exception ex)
        {
            // Handle other exceptions
            Console.WriteLine("An unexpected error occurred: " + ex.Message);
        }
        finally
        {
            // Cleanup code, regardless of whether an exception occurred
            Console.WriteLine("Cleaning up...");
        }
    }   

```

There are 3 ways to re-throw an exception:

- **`throw**` — rethrows the current exception and preserves the stack trace.
- **`throw ex`** — throws an existing exception but resets the stack trace from the point of rethrow.
- **`throw new Exception**` — creates a new exception, which completely rewrites the stack trace.

## **Handling Exceptions With Filtering in C#**

The `when` clause in C# catch blocks allows as to specify a condition that if true it will be handled in the catch block. 

This functionality, termed **exception filtering**, proves beneficial in situations where exceptions should only be caught under specific circumstances.

We can employ a **`when`** clause to handle various instances of **`FormatException`** differently:

```csharp
try
{
   var result = ConvertToNumber("123abc");
   Console.WriteLine($"Processing result: {result}");
}
catch (FormatException ex) when (ex.Message.Contains("invalid format"))
{
   Console.WriteLine("Data has an invalid format. Please check your inputs.");
}
catch (FormatException ex) when (ex.Message.Contains("empty string"))
{
   Console.WriteLine("No data provided. Please enter some numeric data.");
}
catch (OverflowException ex) when (ex.Message.Contains("too large"))
{
   Console.WriteLine("Data is too large. Please enter a smaller number.");
}
catch (Exception ex)
{
   Console.WriteLine($"An unexpected error occurred: {ex.Message}");
}

```

## Best practices for exception handling in C#

- **Avoid throwing generic exceptions**:
    
    Throwing generic exceptions like **`System.Exception`** can make it difficult to determine the root cause of an issue. Instead, use specific exception types to provide more context and make debugging easier.
    
- **Avoid swallowing exceptions**:
    
    Swallowing exceptions by catching them and not doing anything with them can hide problems in your code and make debugging challenging. At the very least, log exceptions so you have a record of what went wrong.
    
- **Handle exceptions at the appropriate level**:
    
    Handle exceptions at the appropriate level of abstraction in your code. For example, catch exceptions at the UI layer for user-facing errors and catch them closer to the source for technical errors.
    
- **Provide meaningful error messages**:
    
    When throwing exceptions, provide meaningful error messages that explain what went wrong and how to resolve the issue. This makes it easier for developers to understand and address problems.
    

Here's an example of handling a specific scenario using these best practices:

We have a method that loads data from a file. 

We catch specific exceptions like **`FileNotFoundException`** and **`IOException`** to handle file-related errors and inform the user appropriately. 

For any other unexpected errors, we log the exception and inform the user to contact support for assistance. 

Finally, we rethrow the exception to ensure it's not swallowed and can be handled elsewhere if necessary.

```csharp
public void LoadDataFromFile(string filePath)
{
    try
    {
        // Attempt to load data from the file
        var data = File.ReadAllText(filePath);
        ProcessData(data);
    }
    catch (FileNotFoundException ex)
    {
        // Log the exception and inform the user that the file does not exist
        Log.Error(ex, "File not found: {FilePath}", filePath);
        MessageBox.Show($"The file '{filePath}' does not exist.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
    }
    catch (IOException ex)
    {
        // Log the exception and inform the user that there was an error reading the file
        Log.Error(ex, "Error reading file: {FilePath}", filePath);
        MessageBox.Show($"An error occurred while reading the file '{filePath}'. Please try again later.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
    }
    catch (Exception ex)
    {
        // Log the exception and inform the user that an unexpected error occurred
        Log.Error(ex, "An unexpected error occurred while loading data from file: {FilePath}", filePath);
        MessageBox.Show("An unexpected error occurred. Please contact support for assistance.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
        throw; // Rethrow the exception to ensure it's not swallowed
    }
}
```