DEV Community

Cover image for Mastering Exception Handling in C#: A Comprehensive Guide
Anton Martyniuk
Anton Martyniuk

Posted on • Originally published at antondevtips.com on

Mastering Exception Handling in C#: A Comprehensive Guide

Exception handling is a critical component of software development in C#.
It allows to gracefully manage errors, ensuring applications remain stable and user-friendly under unforeseen circumstances.
This guide provides a comprehensive look at exception handling in C#, covering everything from basic try/catch blocks to throwing custom exceptions.

Originally published at https://antondevtips.com.

Using Try/Catch for Exception Handling in C

The try/catch block is the foundation of exception handling in C#.
It allows you to "try" a block of code and "catch" any exceptions that might be thrown.
Here's how you can implement it:

try
{
    var a = 5;
    var b = 0;
    Console.WriteLine(a / b);
}
catch (DivideByZeroException ex)
{
    Console.WriteLine("Error: " + ex.Message);
}
Enter fullscreen mode Exit fullscreen mode

This code will catch the DivideByZeroException and prevent the application from crashing by displaying an error message:
Error: Attempted to divide by zero.

Using Try/Catch/Finally for Exception Handling in C

Adding a finally block allows you to execute code regardless of whether an exception was thrown or not.
This is especially useful for cleaning up resources, such as closing file streams or database connections:

try
{
    var file = File.Open("file.txt", FileMode.Open);
    file.Close();
}
catch (FileNotFoundException ex)
{
    Console.WriteLine("File not found: " + ex.Message);
}
finally
{
    Console.WriteLine("Executing finally block");
}
Enter fullscreen mode Exit fullscreen mode

Finally block is executed after all code is executed from try and corresponding catch blocks.

Using Try/Finally for Exception Handling in C

A try/finally block without a catch is useful when you want to ensure cleanup happens, but prefer to let an exception propagate up the call stack:

public void OpenFile()
{
    try
    {
        // Code that might need cleanup
        var file = File.Open("file.txt", FileMode.Open);
        file.Close();
    }
    finally
    {
        Console.WriteLine("Executing finally block");
    }
}
Enter fullscreen mode Exit fullscreen mode

An OpenFile function throws FileNotFoundException exception and executes a finally block.
Here an exception is not suppressed and is thrown up the call stack.
Eventually it is handled by a catch statement around OpenFile function call:

try
{
    OpenFile();
}
catch (Exception ex)
{
    Console.WriteLine("Caught error from OpenFile() function: " + ex.Message);
}
Enter fullscreen mode Exit fullscreen mode

Handling Multiple Exception in C

Let's explore a use case when an application reads data from a file and processes it.
This operation can encounter various issues, such as the file not existing, the file format being incorrect, or the content data that can't be processed correctly.
Here's how you can handle these different exceptions:

try
{
    var filePath = "data.txt";
    var fileContent = File.ReadAllText(filePath);
    var data = ParseData(fileContent);
    ProcessData(data);
}
catch (FileNotFoundException ex)
{
    Console.WriteLine("The file was not found: " + ex.Message);
}
catch (FormatException ex)
{
    Console.WriteLine("Data format is incorrect: " + ex.Message);
}
catch (Exception ex)
{
    Console.WriteLine("An unexpected error occurred: " + ex.Message);
}
Enter fullscreen mode Exit fullscreen mode

Here exceptions are handled by catch blocks from the top to the bottom.
When an exception matches a certain catch condition - it is handled by this block and other catch blocks are omitted.

This is the flow of exception handling in the example above:

  1. FileNotFoundException: This catch block handles the scenario where the file does not exist at the specified path.
  2. FormatException: This is used when the format of the data in the file isn't what the application expects.
  3. General Exception: The last catch block is a more general one, which will catch any other types of exceptions that weren't previously handled. This is useful for catching unexpected exceptions that you might not have foreseen while writing the code.

Handling Exceptions With Filtering in C

The when clause in C# catch blocks allows you to specify a condition that must be true for the particular catch block to handle the exception.
This feature, known as exception filtering, can be useful for catching exceptions only when specific conditions are met.

Let's explore an example of a method that converts string to a number:

private int ConvertToNumber(string input)
{
    if (string.IsNullOrEmpty(input))
    {
        throw new FormatException("empty string");
    }

    if (!int.TryParse(input, out var number))
    {
        throw new FormatException("invalid format");
    }

    if (number > int.MaxValue)
    {
        throw new OverflowException("too large");
    }

    return number;
}
Enter fullscreen mode Exit fullscreen mode

You can handle different variations of FormatException using a when clause:

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}");
}
Enter fullscreen mode Exit fullscreen mode

How To Throw Exceptions in C

When catching an exception, after handling you might need to re-throw this exception up the call stack.

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.

Here is how you can rethrow an exception:

try
{
    var file = File.Open("file.txt", FileMode.Open);
    file.Close();
}
catch (FileNotFoundException ex)
{
    Console.WriteLine("File not found: " + ex.Message);

    throw;
    // throw ex;
    // throw new Exception("File was not found");
}
Enter fullscreen mode Exit fullscreen mode

The preferred way in the most of the use cases is using throw; as it preserves an original stack trace.
Use other options only when appropriate.

How to Modify Exception and Rethrow It ?

If you need to modify an exception while preserving the stack trace, you can add additional properties to the Data property of the exception:

try
{
    var file = File.Open("file.txt", FileMode.Open);
    file.Close();
}
catch (FileNotFoundException ex)
{
    ex.Data.Add("ExtraInfo", "Details here");
    throw;
}
Enter fullscreen mode Exit fullscreen mode

Another option is to throw a new exception that will contain an original exception as its inner exception.
Inner exceptions allow developers to track back through the exception chain and understand the sequence of events that led to a problem, especially when exceptions are rethrown with additional context.

try
{
    // Attempt to open and close a file
    var file = File.Open("file.txt", FileMode.Open);
    file.Close();
}
catch (FileNotFoundException ex)
{
    // Create a new exception, passing the original one as an inner exception
    throw new ApplicationException("An error occurred while trying to open the file.", ex);
}
Enter fullscreen mode Exit fullscreen mode

The original FileNotFoundException is passed as an inner exception to the new ApplicationException.
This approach keeps the stack trace and the original error message intact, which can be important for debugging.
It provides a clear, nested structure showing that the ApplicationException was directly caused by the FileNotFoundException.

Handling Exceptions in async/void methods

Handling exceptions in async void methods is impossible because these exceptions cannot be caught outside the method.
You can only catch exceptions within the method but not up the stack trace:

public async void OpenFileAsync()
{
    try
    {
        using var fileStream = await File.OpenAsync("file.txt", FileMode.Open);
        Console.WriteLine("File opened successfully.");
    }
    catch (FileNotFoundException ex)
    {
        Console.WriteLine("Failed to open file: " + ex.Message);
        throw;
    }
}
Enter fullscreen mode Exit fullscreen mode

When file is not found, you can catch the exception within the OpenFileAsync method.
If you rethrow an exception or simply miss the catch statement - you won't be able to catch this exception.
This can lead to unexpected application crashes or UnobservedTaskException in the TaskScheduler.

Async/void is known evil in C# and should be omitted in all costs.
The only exclusion are standard EventHandlers that have void return type by design that can't be changed.

How to Throw Custom Exceptions

In C# you can create and throw your own custom exceptions.
Custom exceptions let you define specific error details and behaviors that are relevant to your logic.
They are particularly useful in libraries.

Imagine you're implementing a library that is fetching users from some kind of the data store.
If a user is not found - you can throw a custom UserNotFoundException:

public async Task<User> GetUserByEmailAsync(string email)
{
    var user = await FindUserByEmailAsync(email);
    if (user is null)
    {
        throw new UserNotFoundException($"User with email {email} was not found.");
    }

    return user;
}
Enter fullscreen mode Exit fullscreen mode

To define a custom exception, you need to inherit from the base Exception class:

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

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

    public UserNotFoundException(string message, Exception inner)
        : base(message, inner)
    {
    }
}
Enter fullscreen mode Exit fullscreen mode

As a general practise it is recommended to implement 3 types of exception constructors:

  • parameterless
  • with a single message parameter
  • with a message and inner exception parameters

Best Practises for Exception Handling in C

  • Specific before general: Always catch more specific exceptions before the more general ones. This ensures that each exception is handled as specifically as possible.
  • Minimize exception handling code: Only use exception handling for scenarios where you expect something might go wrong due to circumstances beyond your control (e.g., file I/O operations, network requests, database access, etc.). Avoid using exceptions for control flow, except the libraries.
  • Log detailed information: When catching exceptions, log as much detail as is safely possible. This can help with debugging and understanding the context in which errors occurred.

Hope you find this blog post useful. Happy coding!

Originally published at https://antondevtips.com.

After reading the post consider the following:

  • Subscribe to receive newsletters with the latest blog posts
  • Download the source code for this post from my github (available for my sponsors on BuyMeACoffee and Patreon)

If you like my content —  consider supporting me

Unlock exclusive access to the source code from the blog posts by joining my Patreon and Buy Me A Coffee communities!

Top comments (0)