Catch, Rethrow, and Debuggability

When you catch and rethrow an exception, the stack is unwound by the time the catch clause is executed, and thus you may lose valuable information in the debugger.

Consider the following code snippet:

     static void Main(string[] args)
    {
        Func1a();
    }

    static void Func1a()
    {
        try
        {
            Func2();
        }
        catch
        {
            throw; // rethrown, this goes unhandled
        }
    }

    static void Func2()
    {
        int x = 5;
        throw new Exception("Test"); // original first chance exception
    }

If you stop in the debugger when the exception is first thrown, you get the full callstack right at the throw point :

   random.exe!Program.Func2() C#
  random.exe!Program.Func1b() C#
  random.exe!Program.Main(string[] args = {Dimensions:[0]}) C#

You can then use the full power of the debugger, such as inspecting parameters, locals,  interceptable exceptions, mixed-mode callstacks (if interop-debugging), native assembly windows, etc.

However, if you stop at the catch/rethrow point, the stacktrace in the debugger is:

   random.exe!Program.Func1b() C#
  random.exe!Program.Main(string[] args = {Dimensions:[0]}) C#

Now Func2(), the original throw site, is no longer on the stack in the debugger. 

Other trivia:

  1. What about the Exception.StackTrace property ? Exceptions have a StackTrace property which is a string representation of the stack trace. It is not nearly as rich as the stack trace under the debugger as mentioned above.

  2. 'throw;' vs. 'throw e;' ?  These affect the exception object that's thrown, but don't affect the callstack from which it's thrown. Imagine this scenario:

             catch (Exception e)
            {            
                if (...) 
                    throw;
                else
                    throw e;
            }
    

    The callstack would be determined by the time you entered the catch block, and so would be the same in both cases.

  3. Does it have to be this way? This is a natural implementation resulting from Structured Exception Handling (SEH) because catch handlers are executed in the 2nd-pass. See Matt Pietrek's excellent SEH article for more gory details. You could, of course, imagine alternative implementations.

  4. What about unmanaged code? This is also true for unmanaged code (see Matt's article). However, unmanaged C++ exceptions don't even have stacktrace properties, so that case is even harder to debug.

  5. The example above uses catch-all (which is frowned upon); but this rethrow problem is also true for catching specific exceptions.

  6. [Update 2/12/07]:  Here's a technique using JMC to make catch / rethrow more debuggable.

The practical side:

  1. Some library functions will automatically catch + rethrow. For eg:
    - MethodInfo.Invoke will catch and rethrow exceptions as a TargetInvocationException, with an inner exception of the original exception.
    - Exceptions crossing appdomain boundaries will get caught + marshaled across the appdomain boundary + rethrown  on the other side.
  2. People commonly get burned by this because most people (for good reason) don't stop on first-chance exceptions. So by the time you gets the unhandled exception notification, the useful info has already been lost via catch/rethrow.  
  3. This can be especially tough for unhandled exception bug reports.

Comments

  • Anonymous
    February 07, 2007
    Nice collection of usually disparate information. One thing I would add is that you sometimes want to make use of the stack-hiding side effects of rethrow.  If you're calling into security-related code, or making use of code that sends sensitive information as parameters (i.e. on the stack) a catch/rethrow will make sure that sensitive information isn't propagated.

  • Anonymous
    February 07, 2007
    Peter - That's a good reason to use catch/rethrow for an app; ignoring  the debugger. I want to be clear to others thought that the debugger has complete control over a debuggee. While an app can hide things from other parts of the app (via CAS, etc), the debuggee can't hide things from a determined debugger. Regardless, as cute trivia, if you really want to hide the stack, consider: Exception e = null; try {   SecuritySensitiveMethod(); } catch(Exception e2) {   e = e2; } if (e != null) throw MyNewException(e);

  • Anonymous
    February 07, 2007
    Is there a way to make the debugger stop for the case above even if that is not my code? I find that I need that fairly often, when exception redirection is done two layers too deep and I don't get the correct information

  • Anonymous
    February 07, 2007
    The comment has been removed

  • Anonymous
    February 11, 2007
    Mike, FWIW, some trivia... The stack isn't "lost" if the original exception is rethrown as the inner exception of a new exception object, which chains the exception objects together. Using a catch-wrap-throw pattern ensures that the original stack trace is available, even across multiple catch--wrap-throws.   try    {    Func();    }   catch(Exception ex)    {    throw new Exception("msg",ex);         }    } The stack trace of the outermost exception will begin at the callsite of the last throw statement, but you can examine the inner exception for the original stack trace. A routine that unrolls all the exceptions and merges the stack traces can produce an output that looks like a single stack trace. (This also makes it possible to add context info as part of the message at each rethrow point) The debugger makes it easy to access the inner exceptions, all the way back to the callsite of the original throw statement, and this will work even if the exception is eventually unhandled.

  • Anonymous
    February 12, 2007
    DAL - what you're talking about is the Exception.StackTrace property that I mentioned above. That's a useful thing, and it's nice you have inner exceptions to chain them together.   throw new Exception("msg",ex); and   throw; are very similar with this respect. My point is that the stack has been unwound by the time the catch is executing (and thus I'd say "lost"). You have a string representation via the StackTrace property, but that falls short of being able to actually view it in the debugger's callstack window and do the things I mentioned above.

  • Anonymous
    February 12, 2007
    You are, of course, correct. The ability of the debugger to actually walk back up the call stack is gone once the stack has been unwound, which it has been by the time the catch handler is invoked. You only get the full power of the debugger in the context of the call. Your original comments, as well as some of the responders, mentioned the stacktrace property, which is why I wrote about this other aspect of it. The stack trace can preserve the text representation of call stack, but that is not the same as actual call stack.

  • Anonymous
    February 12, 2007
    I previously mentioned that catch / rethrow an exception impedes debuggability because the callstack

  • Anonymous
    February 14, 2007
    So what are the usefull differences between "throw" and "throw e"? in: catch(Exception e) {  if(test)    throw;  else    throw e; }

  • Anonymous
    February 15, 2007
    Someone asked what the practical differences are between 'throw;' (no arguments) and 'throw object;'

  • Anonymous
    February 15, 2007
    Brian - Nobody should ever write code like that. I was just using it as an academic example to proove a point about callstack unwinding.  I should have been clearer. Here is more comparison between throw + throw e: http://blogs.msdn.com/jmstall/archive/2007/02/15/throw-vs-rethrow.aspx

  • Anonymous
    February 28, 2007
    Very nice site! Good work.

  • Anonymous
    March 09, 2007
    Nice design, good graphical content. I think I'll come back later again;)

  • Anonymous
    October 05, 2007
    I just noticed that my blog had birthday #3 (Sep 30th) . In tradition, some various stats... 384 posts.

  • Anonymous
    March 23, 2009
    It's time for another episode of Good Idea / Bad Idea: Good Idea! Catching a specific exception from

  • Anonymous
    March 29, 2009
    Anders Hejlsberg,C#和.NET框架背后的创造型天才,在12年前加入微软之前,他就以编译器编写者的身份驰名16年了。他的BLS Pascal、Turbo Pascal和Delphi彻底变革了软件开发方式。今天,他依然能够冒出新的想法和激进的倡议。