C# exception handling best practices

I'm getting near my 20th anniversary in the tech industry. During the years, I have seen almost every anti-pattern when dealing with exceptions (and made the mistakes personally as well). This post contains a collection of my best practices when dealing with exceptions in C#.

Decorate exceptions

I see this used way to rarely. All exceptions extend Exception, which has a Data dictionary. The dictionary can be used to include additional information about an error. Whether or not this information is visible in your log depends on what logging framework and storage you are using. For elmah.io, Data entries are visible in the Data tab within elmah.io.

To include information in the Data dictionary, add key/value pairs:

var exception = new Exception("En error happened");
exception.Data.Add("user", Thread.CurrentPrincipal.Identity.Name);
throw exception;

In the example, I add a key named user with a potential username stored on the thread.

You can decorate exceptions generated by external code too. Add a try/catch:

try
{
    service.SomeCall();
}
catch (Exception e)
{
    e.Data.Add("user", Thread.CurrentPrincipal.Identity.Name);
    throw;
}

The code catches any exceptions thrown by the SomeCall method and includes a username on the exception. By adding the throw keyword to the catch block, the original exception is re-thrown.

Catch the more specific exceptions first

You know you have code similar to this:

try
{
    File.WriteAllText(path, contents);
}
catch (Exception e)
{
    logger.Error(e);
}

Simply catching Exception and logging it to your preferred logging framework is quick to implement and get the job done. Most libraries available in .NET can throw a range of different exceptions, and you might even have a similar pattern in your code-base. Catching multiple exceptions ranging from the most to the least specific error is a great way to differentiate how you want to continue on each type.

Automatically log exceptions with elmah.io

➡️ Get started now - no credit card required ⬅️

In the following example, I'm explicit about which exceptions to expect and how to deal with each exception type:

try
{
    File.WriteAllText(path, contents);
}
catch (ArgumentException ae)
{
    Message.Show("Invalid path");
}
catch (DirectoryNotFoundException dnfe)
{
    Message.Show("Directory not found");
}
catch (Exception e)
{
    var supportId = Guid.NewGuid();
    e.Data.Add("Support id", supportId);
    logger.Error(e);
    Message.Show($"Please contact support with id: {supportId}");
}

By catching ArgumentException and DirectoryNotFoundException before catching the generic Exception, I can show a specialized message to the user. In these scenarios, I don't log the exception since the user can quickly fix the errors. In the case of an Exception, I generate a support id, log the error (using decorators as shown in the previous section) and show a message to the user.

Please notice that while the code above serves the purpose of explaining exception order, it is a bad practice to implement control flow using exception like this. Which is a perfect introduction to the next best practice:

Avoid exceptions

It may sound obvious to avoid exceptions. But many methods that throw an exception can be avoided by defensive programming.

One of the most common exceptions is NullReferenceException. In some cases, you may want to allow null but forget to check for null. Here is an example that throws a NullReferenceException:

Address a = null;
var city = a.City;

Accessing a throws an exception but play along and imagine that a is provided as a parameter.

In case you want to allow a city with a null value, you can avoid the exception by using the null-conditional operator:

Address a = null;
var city = a?.City;

By appending ? when accessing a, C# automatically handles the scenario where the address is null. In this case, the city variable will get the value null.

Another common example of exceptions is when parsing numbers or booleans. The following example will throw a FormatException:

var i = int.Parse("invalid");

The invalid string cannot be parsed as an integer. Rather than including a try/catch, int provides a fancy method that you probably already used 1,000 times:

if (int.TryParse("invalid", out int i))
{
}

In case invalid can be parsed as an int, the TryParse returns true and put the parsed value in the i variable. Another exception avoided.

Create custom exceptions

It's funny to think back on my years as a Java programmer (back when .NET was in beta). We created custom exceptions for everything. Maybe it was because of the more explicit exception implementation in Java, but it's a pattern that I don't see repeated that often in .NET and C#. By creating a custom exception, you have much better possibilities of catching specific exceptions, as already shown. You can decorate your exception with custom variables without having to worry if your logger supports the Data dictionary:

public class MyVerySpecializedException : Exception
{
    public MyVerySpecializedException() : base() {}
    public MyVerySpecializedException(string message) : base(message) {}
    public MyVerySpecializedException(string message, Exception inner) : base(message, inner) {}
    
    public int Status { get; set; }
}

The MyVerySpecializedException class (maybe not a class name that you should re-use :D) implements three constructors that every exception class should have. Also, I have added a Status property as an example of additional data. This will make it possible to write code like this:

try
{
    service.SomeCall();
}
catch (MyVerySpecializedException e) when (e.Status == 500)
{
    // Do something specific for Status 500
}
catch (MyVerySpecializedException ex)
{
    // Do something general
}

Using the when keyword, I can catch a MyVerySpecializedException when the value of the Status property is 500. All other scenarios will end up in the general catch of MyVerySpecializedException.

Log exceptions

This seem so obvious. But I have seen too much code failing in the subsequent lines when using this pattern:

try
{
    service.SomeCall();
}
catch
{
    // Ignored
}

Logging both uncaught and catched exceptions is the least you can do for your users. Nothing is worse than users contacting your support, and you had no idea that errors had been introduced and what happened. Logging will help you with that.

There are several great logging frameworks out there like NLog and Serilog. If you are an ASP.NET (Core) web developer, logging uncaught exceptions can be done automatically using elmah.io or one of the other tools available out there.

Features steps
We monitor your websites

We monitor your websites

We monitor your websites for crashes and availability. This helps you get an overview of the quality of your applications and to spot trends in your releases.

We notify you

We notify you

We notify you when errors starts happening using Slack, HipChat, mail or other forms of communication to help you react to errors before your users do.

We help you fix bugs

We help you fix bugs

We help you fix bugs quickly by combining error diagnostic information with innovative quick fixes and answers from Stack Overflow and social media.

See how we can help you monitor your website for crashes Monitor your website