Advanced Exception Handling in C# and .NET Applications

In 2018, Backtrace created a crash and exception handling library with C# and .NET that allows developers to better understand an exception generated by any .NET application. In this article, we’ll share our knowledge so that you can handle errors and deliver any .NET application to your customers more efficiently and effectively.

We’ll illustrate a few methods for handling specific C# errors based on our experience working with real-world applications from customers using the Backtrace C# Library. We’ll cover six, key issues that developers often encounter while debugging code and I’ll offer suggestions for making the code easier to debug.

  1. Capture the full stack trace when throwing exceptions in C#
  2. Environment stack trace vs. exception stack trace
  3. Get the exception type from the exception object
  4. Use Exception Filters to catch different types of exceptions
  5. Handle aggregate exceptions
  6. Get line numbers in production code stack traces

Throughout the article, we’ll refer back to this example of a simple application that tries to parse a string of arguments:

private static void Bar(string pathToFile, string repeatString)
using (var writer = new StreamWriter(pathToFile, true))
int.TryParse(repeatString, out int repeat);
for (int i = 0; i < repeat; i++)
writer.WriteLine("wubba lubba dub dub");

private static void Foo(string pathToFile, string repeat)
Bar(pathToFile, repeat);
catch(Exception e)
throw e;

private static void Main(string[] args)
string pathToFile = args[0];
string repeat = args[1];
Foo(pathToFile, repeat);

Capture the full stack trace when throwing exceptions in C#

Look at the try/catch block of code in our example app above. When preparing exception handling code, a developer will recognize the following exception, printed in standard output, is missing important information.

Missing Stack Trace

The information above doesn’t tell us exactly where the exception occurred. We know what the issue is because we can still use the information to find the issue. But, the generated stack trace doesn’t contain the most important stack frames.

In our sample code, we’re trying to rethrow an exception from the try/catch block by using the command throw e;. This means the throw command will recreate a stack trace from wherever the developer uses the command. So, if you receive an exception and rethrow it, you’ll miss stack frames from that try/catch block to exception.

How can we rethrow an exception with missing stack frames? In the try/catch block, you can use a throw command to automatically rethrow exception from the try/catch to calling methods. Let’s see what the output from our sample code would look like when we replace throw e; with throw;

Complete Stack Trace

With this method, we’ll understand much more about our exceptions, namely, the cause—the tryParse method. Next, we’ll navigate faulty code to fix existing issues.

Environment Stack Trace vs Exception Stack Trace

In the example above, our exception wasn’t handled by a try/catch block after the rethrow. When we tried to print an exception from the first try/catch block, we missed stack frames.

This happens because an exception object only stores stack frames from a try/catch block. So, how do we get stack trace information from an application start to a try/catch block? With .NET we can create a StackTrace object that will collect stack frames from the app start to the current moment:

var stackTrace = new StackTrace();

With the code above, we can recreate the steps so that the application creates an exception from the beginning of the try/catch block.

When we created a stack trace parser in our Backtrace C# library, we added support for environment stack frames. Later, we realized that most stack frames aren’t so useful. Modern .NET requires asynchronous code with async/await. The async state machine generates many useless stack frames that won’t allow you to pinpoint what caused your app to throw an exception. Even if you clean the generated stack trace (by removing stack frames from Microsoft libraries) it still won’t give developers useful information.

This taught us the importance of a try/catch block. If you still need to include an environment stack trace, you can add a stack frame to the existing stack trace. Note that by removing the stack frames and customizing the modification, you change how the deduplication algorithm works. For other exceptions, we suggest using the global exception handler (see backtraceClient.HandleApplicationException()), which should catch all other exceptions.

Getting Exception Type Information From the Exception Object

In the previous example, we used an exception classifier and exception message to determine root cause. Stack frames allowed us to localize faulty code, and now we can change the wrong code easily.

In some cases, we might want to reprocess exception information but still capture all possible exceptions.

To do so, we can use an exception filter (more on this later) or use a reflection to get the exception type (classifier) from the exception object in a try/catch block:

var exceptionType = e.GetType().FullName;

Reflection gives us much more information about an exception. If you aren’t familiar with the reflection mechanism in C#, check out the gif below for an example of an API reflection.

Execution Types

Our C# library, using the reflection available property, allows us to generate a more thorough report. The reflection mechanism, in this case, allows us to collect program attributes and detect faulting assembly name.

Using Exception Filters to Catch Different Types of Exceptions

C# 6.0 allows you to create an exception filter. What does it mean? You are probably familiar with syntax, where you can use multiple catch to handle different types of exceptions. You might want to use different catch clauses for the same exception based on an exception property. It could be solved using the reflection method described above, but you can also use the new C# feature—Exception Filters.

If you need more control on exception handling, you can write simple arguments to invoke a specific catch block. In this case, I prefer to filter the exception by a method that returns true/false for a specific exception type. How can I use exception filters in my application? It’s simple –change the exception handling code to include a when clause:

//faulting code
catch (WebException e) when (e.Status == WebExceptionStatus.SendFailure)
//do something
catch (WebException e) when (e.Status == WebExceptionStatus.Timeout)
//do something else
catch (Exception e)
//hmmm something goes wrong :O

Handling Aggregate Exceptions

If you’re familiar with an Entity Framework and ASP.NET you probably receive one specific type of exception—AggregateException. This type of exception aggregates one or more exceptions in the exception container. AggregateException has the property InnerException that contains all the exception objects.

When we developed Backtrace’s support for .NET, we decided to ensure support for every type of exception, including AggregateException. In our early releases, we recognized AggregateException reports didn’t provide enough information for our users. Why? Because they were only receiving information about existing exceptions, nothing more. So, we decided to ensure support for unpacking exceptions from AggregateException to give developers more detailed information.

Getting Line Numbers in Production Code Stack Traces

With minor modifications to our code, we can navigate to the faulting source code and change the incorrect library behavior.

When you share your application with special .pdb files, the release mode of your application will not generate a stack trace with the line number information.

When you debugged your application locally, Visual Studio used .pdb files to provide line number information for your stack trace. Now, when you compile and make the release build of your application, the .NET platform cannot use missing .pdb files. This will cause a missing stack trace and you won’t be able to pinpoint where your code failed.

Backtrace C# integration provides ILOffset a property that contains information to address the faulty code. With this information, you can use the .pdb file to find the issue.

For deeper introspection, Backtrace also provides minidump support. You can use this file in the Backtrace Debugger or downloaded it to Visual Studio to learn:

  • Where your code throws exception
  • The value of variables in the current scope
  • The application state?


We’ve illustrated a few common issues developers face when trying to debug applications. We hope these methods make your code more efficient and safe.

Learn more about Backtrace’s C# reporting library.


By | 2019-11-03T16:53:25+00:00 April 15th, 2019|Backtrace, Engineering, Technical Details|