Mastering Exception Handling in C#: A Comprehensive Guide
Tags: dotnet, csharp, backend, programming
Published: 2024-05-15 11:16:37

---
canonical_url: https://antondevtips.com/blog/mastering-exception-handling-in-csharp-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_](https://antondevtips.com/blog/mastering-exception-handling-in-csharp-a-comprehensive-guide)_._

## 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:

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

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:

```csharp
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");
}
```

`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:

```csharp
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");
	}
}
```

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:

```csharp
try
{
	OpenFile();
}
catch (Exception ex)
{
	Console.WriteLine("Caught error from OpenFile() function: " + ex.Message);
}
```

## 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:

```csharp
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);
}
```

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:

```csharp
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;
}
```

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

```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}");
}
```

## 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:

```csharp
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");
}
```

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:

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

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.

```csharp
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);
}
```

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:

```csharp
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;
    }
}
```

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`:

```csharp
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;
}
```

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

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

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

	public UserNotFoundException(string message, Exception inner)
		: base(message, inner)
	{
	}
}
```

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_](https://antondevtips.com/blog/mastering-exception-handling-in-csharp-a-comprehensive-guide)_._

### After reading the post consider the following:

- [Subscribe](https://antondevtips.com/blog/mastering-exception-handling-in-csharp-a-comprehensive-guide#subscribe) **to receive newsletters with the latest blog posts**
- [Download](https://github.com/AntonMartyniuk-DevTips/dev-tips-code/tree/main/backend/CSharp/CSharp_Exceptions) **the source code for this post from my** [github](https://github.com/AntonMartyniuk-DevTips/dev-tips-code/tree/main/backend/CSharp/CSharp_Exceptions) (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!

[![](https://cdn-images-1.medium.com/max/233/1*GSZFx05RDUWr5pn_E9bK8w.png)](https://www.buymeacoffee.com/antonmartyniuk)

[![](https://cdn-images-1.medium.com/max/229/1*8tJk9eYCJrkzoAJw2cILSA.png)](https://www.patreon.com/bePatron?u=73769486)