Preserving Stack Traces When Re-Throwing Inner Exceptions

An entry about c# 3.0 Publication date 14. March 2008 21:54

In the AOP framework I've been blogging about lately, all member invocations are done through reflection. One side-effect of this, is that if you have any exception handling in your application which expects some specific exception type, it will fail because all exceptions thrown are wrapped in a TargetInvocationException as a result of the reflection. Now the solution might sound simple - the framework should just catch any TargetInvocationExceptions and 'unwrap' them, re-throwing the inner exception instead. There's one huge pitfall here though - catching and re-throwing an exception causes the stack trace to be reset.

Let's look at an example. Here's a program that'll reproduce the scenario:

using System;
using System.Reflection;
 
namespace PreserveStackTrace
{
    class Program
    {
        static void Main(string[] args)
        {
            InvokeRealMethodByReflection(args);
        }
 
        public static void RealMethod(object arg)
        {
            throw new ArgumentNullException("arg");
        }
 
        private static void InvokeRealMethodByReflection(string[] args)
        {
            MethodInfo method = typeof(Program).GetMethod("RealMethod");
 
            object arg = null;
 
            method.Invoke(null, new object[] { arg });
        }
    }
}

 

Running this, we get a "TargetInvocationException was unhandled" error, with the ArgumentNullException as the inner exception, with the following stack trace:

at ConsoleApplication3.Program.RealMethod(Object arg) in {...}\\Program.cs:line 15

 

With line 15 being the "throw new ArgumentNullException("arg");" in the above code (count them if you want :p). We don't want a TargetInvocationException to be thrown, though; we want the ArgumentNullException to be the one the calling code has to deal with.

Re-Throw to the Re-scue?

To fix this, we might try refactoring the InvokeRealMethodByReflection method to the following:

private static void InvokeRealMethodByReflection(string[] args)
{
    MethodInfo method = typeof(Program).GetMethod("RealMethod");
 
    object arg = null;
 
    try
    {
        method.Invoke(null, new object[] {arg});
    }
    catch(TargetInvocationException ex)
    {
        throw ex.InnerException;
    }
}

 

Here, we've unwrapped the TargetInvocationException and re-thrown the inner exception. Thus, the exception that bubbles up to the Main method is the ArgumentNullException - but if we look at the stack trace, we've lost valuable information about the where it occurred:

at PreserveStackTrace.Program.InvokeRealMethodByReflection(String[] args) in {..}\Program.cs:line 30
   at PreserveStackTrace.Program.Main(String[] args) in {..}\Program.cs:line 10
   at System.AppDomain._nExecuteAssembly(Assembly assembly, String[] args)
   at System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)
   at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
   at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.ThreadHelper.ThreadStart()

 

According to this, the cause of the exception was the InvokeRealMethodByReflection method, on line 30 - but that is the "throw ex.InnerException;" line! We've lost the information we had earlier about what was really the offending method and line of code, because the stack trace got reset when we re-threw the inner exception.

Solving Our Reflection Woes with More Reflection

A mountain bike mechanic I once knew used to say that if a bit of force won't straighten a rim, you're just not applying enough of it. Chris Taylor uses the same mentality to solve the problem I've described here; he's applying a bit more reflection to straighten out the stack trace problem caused by the use of reflection in the first place. His solution is basically to copy across the stack trace from the TargetInvocationException and sticking it into the inner exception before re-throwing it - quite brilliant in it's simplicity (if the rim is bent, just unbend it!).

private static void InvokeRealMethodByReflection(string[] args)
{
    MethodInfo method = typeof(Program).GetMethod("RealMethod");
 
    object arg = null;
 
    try
    {
        method.Invoke(null, new object[] {arg});
    }
    catch(TargetInvocationException ex)
    {
        FieldInfo remoteStackTraceString = typeof(Exception).GetField("_remoteStackTraceString", BindingFlags.Instance | BindingFlags.NonPublic);
        remoteStackTraceString.SetValue(ex.InnerException, ex.InnerException.StackTrace + Environment.NewLine);
 
        throw ex.InnerException;
    }
}

 

Now, if we re-run the app, we get the desired ArgumentNullException and the desired stack trace:

at PreserveStackTrace.Program.RealMethod(Object arg) in {...}\Program.cs:line 15
   at PreserveStackTrace.Program.InvokeRealMethodByReflection(String[] args) in {...}\Program.cs:line 33
   at PreserveStackTrace.Program.Main(String[] args) in {...}\Program.cs:line 10
   at System.AppDomain._nExecuteAssembly(Assembly assembly, String[] args)
   at System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)
   at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
   at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.ThreadHelper.ThreadStart()

Currently rated 5.0 by 4 people

  • Currently 5/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Powered by BlogEngine.NET 1.4.5.0

Welcome!

My name is Fredrik Kalseth, and this is my blog - thanks for visiting! I am fortunate enough to work with what I love for a living, and this blog is essentially the biproduct of that.

I work as a senior consultant for Capgemini, and am also an active participant in the Norwegian .NET community, as an avid attendee but also as a speaker (most recently at NNUG and MSDN Live).

As a developer, I have a wide circle of interest. My primary passion is for agile, test-driven development, with focus on best practices and clean code. That said, I also love to work on the frontend, especially with web development.

On Twitter? My handle is fkalseth. On LinkedIn? I`m there too.

NDC 2010

The conference to attend this summer happens June 16th-18th in Oslo, Norway. Are you going? Be sure to catch my talk on AOP while you're there!

 

Disclaimer

This is a personal blog; any opinions expressed here are my own and do not necessarily reflect those of my employer. All content herein is my own original creation, and as such is protected by copyright law. Unless otherwise stated, all source code posted on this blog is freely usable under the Microsoft Permissive License.

What Readers Talk About

Comment RSS