Rethrowing exceptions and preserving the full call stack trace

时间:2024-09-26 13:33:20

refer:http://weblogs.asp.net/fmarguerie/archive/2008/01/02/rethrowing-exceptions-and-preserving-the-full-call-stack-trace.aspx

http://geekswithblogs.net/sdorman/archive/2007/08/20/Difference-between-quotthrowquot-and-quotthrow-exquot-in-.NET.aspx

Did you know that depending on the way you rethrow exceptions you may lose important information? There are already several blog posts that explain and demonstrate the difference between throw and throw ex. I'm realizing only now that none of the two solutions yields the complete call stack trace information!

Let's see what the problem is and I'll show you the real solution.

I'll use the following method to generate an exception:

private static void BadWork()
{
  int i = 0;
  int j = 12 / i; // Line 10: DivideByZeroException
  int k = j + 1;
}

Let's consider what happens if we call BadWork and rethrow the exception with throw ex as follows:

try
{
  BadWork();
}
catch (Exception ex)
{
  // do something
  // ...
  throw ex; // Line 24
}

Here is the call stack trace that we get in this case:

Unhandled Exception: System.DivideByZeroException: Attempted to divide by zero.
   at Program.WithThrowEx() in Program.cs:line 24
   at Program.Main(String[] args) in Program.cs:line 88

Line 24 is where throw ex is, not where the exception was thrown.

Let's now replace throw ex by throw:

try
{
  BadWork();
}
catch
{
  // do something
  // ...
  throw; // Line 38
}

This time, here is the call stack trace:

Unhandled Exception: System.DivideByZeroException: Attempted to divide by zero.
   at Program.BadWork() in Program.cs:line 10
   at Program.WithThrow() in Program.cs:line 38
   at Program.Main(String[] args) in Program.cs:line 89

As you can see, we get one additional stack frame this time. Line 10 is where the exception was thrown, which is important information because this is the only information that identifies where the exception actually happened.

This shows that it's better to use throw rather than throw ex if you want the full stack trace information to be preserved. However, there are cases where throw is not enough. In the following example, throw does not preserve the full stack trace:

try
{
  int i = 0;
  int j = 12 / i; // Line 47
  int k = j + 1;
}
catch
{
  // do something
  // ...
  throw; // Line 54
}

Unhandled Exception: System.DivideByZeroException: Attempted to divide by zero.
   at Program.WithThrowIncomplete() in Program.cs:line 54
   at Program.Main(String[] args) in Program.cs:line 106

This time, you can see that information is lost again. Line 54 is where throw is, not where the exception was thrown.

To preserve the full call stack information, you need to use the following method:

private static void PreserveStackTrace(Exception exception)
{
  MethodInfo preserveStackTrace = typeof(Exception).GetMethod("InternalPreserveStackTrace",
    BindingFlags.Instance | BindingFlags.NonPublic);
  preserveStackTrace.Invoke(exception, null);
}

This method can be used as follows:

try
{
  int i = 0;
  int j = 12 / i; // Line 78
  int k = j + 1;
}
catch (Exception ex)
{
  // do something
  // ...
  PreserveStackTrace(ex);
  throw; // Line 86
}

Here is the new call stack information you get with the above code:

Unhandled Exception: System.DivideByZeroException: Attempted to divide by zero.
   at Program.WithThrowAndStackTracePreservation() in Program.cs:line 78
   at Program.WithThrowAndStackTracePreservation() in Program.cs:line 86
   at Program.Main(String[] args) in Program.cs:line 110

Here is the call stack information you get with throw ex and a call to PreserveStackTrace:

Unhandled Exception: System.DivideByZeroException: Attempted to divide by zero.
   at Program.BadWork() in Program.cs:line 10
   at Program.WithThrowExAndStackTracePreservation() in Program.cs:line 62
   at Program.WithThrowExAndStackTracePreservation() in Program.cs:line 69
   at Program.Main(String[] args) in Program.cs:line 109

Here we get the full call stack information. Lines 78 and 10 are where the exceptions were thrown. To my knowledge, this is the only way to get complete call stack information in your logs. Without it, it may be difficult to hunt down some bugs.
It's worth noting that if you call PreserveStackTrace, then you can use throw or throw ex and you'll equally get the full stack trace information.

I found this useful trick on Chris Taylor's blog. If you want to use this with .NET 1, you should refer to Chris' post because it seems that the InternalPreserveStackTrace method didn't exist before .NET 2.0.

The complete source code is attached to this post.