If you don't need to be convinced that this is a bug, not a feature, you can skip ahead to the workaround.
The Problem
Imagine a program where Method1 calls Method2 calls Method3 calls Method4 which throws an exception. Imagine that one of these intermediate methods, Method2, needs to catch the exception, do some stuff (not shown), and then rethrow the caught exception.
public static void MyMain()
{
try { Method1(); } catch (Exception e) { Console.WriteLine(e); }
}
public static void Method1()
{
Method2();
}
public static void Method2()
{
try { Method3(); } catch (Exception e) { throw e; }
}
public static void Method3()
{
Method4();
}
public static void Method4()
{
throw new Exception("From Method4");
}
The ultimate caller, MyMain, prints the exception, including it's stack trace. Here's what it prints:
System.Exception: From Method4at MiscCS4Tests.PreserveExceptionStackTrace2.Method1()
at MiscCS4Tests.PreserveExceptionStackTrace2.Method2()
at MiscCS4Tests.PreserveExceptionStackTrace2.MyMain()
Wait, what happened to Method3 and Method4?
The problem is that, when C# throws an exception, it throws away (no pun intended) all stack trace information and starts over.
That is never what I want. (And if it was, I would more likely create a new exception object to throw for other reasons.)
There are ways around this:
- Instead of throw e; use throw; That will preserve the stack trace. But this form of throw is only allowed inside a catch block.
- Create & throw a new exception object that nests the caught exception as the "inner exception". That will retain the full stack trace, if you chain through all the nested exceptions (which Exception.ToString does).
As a typical example, suppose that you catch several different explicit exceptions using different catch blocks, but the body of the catch blocks are identical.
try {
AMethod();
} catch (ArgumentException e) {
DoSomething();
throw;
} catch (IOException e) {
DoSomething();
throw;
}
As a good programmer, you consider duplicated code to be evil, so you refactor it into a shared place. Perhaps a separate method; more often inline like this:
try {
AMethod();
}
catch (ArgumentException e) { ex = e; }
catch (IOException e) { ex = e; }
if (ex != null) {
DoSomething();
throw ex;
}
But however you do it, you're no longer in a catch block when you rethrow. So you have to explicitly specify the exception on the throw. And you've lost your stack trace.
The Workaround
It turns out that Microsoft realized that they needed to preserve (remote) stack traces when rethrowing remote exceptions. So they built that functionality into the Exception class. Too bad they didn't realize the rest of us needed it too; so they made it private.
Here's a handy Exception extension method that will use reflection to invoke this private method.
public static class ExceptionHelper
{
public static void PreserveStackTrace(this Exception e)
{
_internalPreserveStackTrace(e);
}
private static readonly Action<Exception> _internalPreserveStackTrace =
(Action<Exception>)Delegate.CreateDelegate(
typeof(Action<Exception>),
typeof(Exception).GetMethod(
"InternalPreserveStackTrace",
BindingFlags.Instance | BindingFlags.NonPublic));
}
Here's Method2 (from the above example) rewritten to use this:
public static void Method2()
{
try { Method3(); }
catch (Exception e) { e.PreserveStackTrace(); throw e; }
}
And here is the stack trace now:
System.Exception: From Method4
at MiscCS4Tests.PreserveExceptionStackTrace2.Method4() in xxx.cs:line 170
at MiscCS4Tests.PreserveExceptionStackTrace2.Method3() in xxx.cs:line 165
at MiscCS4Tests.PreserveExceptionStackTrace2.Method2() in xxx.cs:line 159
at MiscCS4Tests.PreserveExceptionStackTrace2.Method2() in xxx.cs:line 161
at MiscCS4Tests.PreserveExceptionStackTrace2.Method1() in xxx..cs:line 154
at MiscCS4Tests.PreserveExceptionStackTrace2.MyMain() in xxx..cs:line 149
at MiscCS4Tests.PreserveExceptionStackTrace2.Method4() in xxx.cs:line 170
at MiscCS4Tests.PreserveExceptionStackTrace2.Method3() in xxx.cs:line 165
at MiscCS4Tests.PreserveExceptionStackTrace2.Method2() in xxx.cs:line 159
at MiscCS4Tests.PreserveExceptionStackTrace2.Method2() in xxx.cs:line 161
at MiscCS4Tests.PreserveExceptionStackTrace2.Method1() in xxx..cs:line 154
at MiscCS4Tests.PreserveExceptionStackTrace2.MyMain() in xxx..cs:line 149
Note that Method2 appears twice in the trace: once where the exception was caught, and once where it was thrown. This is undesirable but better than losing the rest of the stack trace.
Caveats:
- There are security restrictions for using reflection. Refer to the Accessing Members That Are Normally Inaccessible section in this article.
- This depends on internal implementations of the Exception class which could change. Although, if that happens, you can at least rewrite this extension method to use less efficient, but publicly supported, interfaces as described here. This involves serializing & deserializing the exception.
Hi Dave,
ReplyDeleteThank you for the information about preserve-stack-trace internal/private method in .NET framework!
Using exception to control-flow is an anti-pattern, and definitely not a good practice. Then, Where would we realistically use this though? Any thoughts?
Thanks!
Hi Shiva,
DeleteYour question is beyond the scope of my post, but here are my thoughts.
Exceptions (like any other programming technique) can be used for good or bad. When using it obscures the common, expected control flow, that is bad. When it removes recurring, verbose error-handling for unusual cases, esp. where that handling needs to be split between much higher and much lower callers, with many intervening layers that could/should be oblivious to such cases, it is good; the common expected control flow becomes clearer.
And there is the pragmatic issue of dealing with third-party APIs (that you have no control over, like .NET) that throw Exceptions.
Given that, whenever you are handling Exceptions for whatever reasons, there is good and bad practice. Other articles discuss this more generally; e.g., https://today.java.net/article/2006/04/04/exception-handling-antipatterns#antipatterns
My post is independent of all that. It says that, if you are using Exceptions, the programming framework defining your base Exceptions should give you the capabilities you need. Specifically, it should make it easy for you to retain all the context information (e.g., stack trace) to enable easier debugging. .NET falls down here.
Thanks for your comment!