Wednesday, April 25, 2007

Re-throwing exceptions

If this hasn't happened to you already, it will in the future.  And when it does, you'll tear your hair out.  You're reading a defect report, an error log, or a message in an event log that tells you an exception was thrown.  The log tells you what type of exception was thrown, the message, and a stack trace.  Here's my exception message:

System.Exception: Something went wrong.
at WindowsApp.Form1.DoWork() in C:\WindowsApp\Form1.cs:line 47
at WindowsApp.Form1..ctor() in C:\WindowsApp\Form1.cs:line 12
at WindowsApp.App.Main() in C:\WindowsApp\App.cs:line 30

I'm feeling good, since I have some good information about where things went wrong.  Let's look at the code:

34: public void DoWork()
35: {
36:     DoSomething();
37:  
38:     try
39:     {
40:         DoSomethingElse();
41:  
42:         DoAnotherThing();
43:     }
44:     catch (Exception ex)
45:     {
46:         LogException(ex);
47:         throw ex;
48:     }
49:  
50:     FinishUp();
51: }
52:  

Looking back at the exception message, it says that the error occurred at line 47.  But line 47 is just the line that re-throws the exception!  Now I'm in big trouble, because I have no idea if it was the DoSomething or the DoAnotherThing that threw the exception.  I could dive in to each method, but who knows how big that rabbit hole goes.  I probably don't have a choice, so down the rabbit hole we go.

The real issue was that I completely lost the stack trace from the exception.  It looks like the CLR created a new stack trace starting at line 47, instead of somewhere inside the try block.  If I'm debugging or fixing code from an exception message, I really need that stack trace.  So how do we prevent this problem?

Re-throwing exceptions

It's actually a very minor change.  See if you can spot the difference:

34: public void DoWork()
35: {
36:     DoSomething();
37:  
38:     try
39:     {
40:         DoSomethingElse();
41:  
42:         DoAnotherThing();
43:     }
44:     catch (Exception ex)
45:     {
46:         LogException(ex);
47:         throw;
48:     }
49:  
50:     FinishUp();
51: }
52:  

Now let's look at the exception message:

System.Exception: Something went wrong.
at WindowsApp.Form1.DoAnotherThing() in C:\WindowsApp\Form1.cs:line 65
at WindowsApp.Form1.DoWork() in C:\WindowsApp\Form1.cs:line 47
at WindowsApp.Form1..ctor() in C:\WindowsApp\Form1.cs:line 12
at WindowsApp.App.Main() in C:\WindowsApp\App.cs:line 30

Now I get the full stack trace.  The exception actually occurred down in the DoAnotherThing method.  I can debug this error with ease, all because I used this:

47: throw;

instead of this:

47: throw ex;

Wrapping exceptions

Often times we don't want specific exceptions showing up in the user interface.  Other times we detect an exception and want to throw a more general exception.  This is called wrapping exceptions, which is catching an exception and throwing a different one:

public void DoWork()
{
    DoSomething();

    try
    {
        DoSomethingElse();

        DoAnotherThing();
    }
    catch (Exception ex)
    {
        LogException(ex);
        throw new ApplicationSpecificException("Something bad happened", ex);
    }

    FinishUp();
}

Note that when I threw a different exception, I passed in the original exception into the constructor.  This preserves the stack trace, and exception messages will display the full stack of the inner exception.  If I didn't pass in the exception, I'd be in the exact same boat as before, not having any idea where to look to debug exceptions because the stack trace would be incorrect.  Since I used the constructor that uses an inner exception, I get to keep my hair.  Of course, the developer who wrote ApplicationSpecificException knows how to correctly implement custom exceptions because they've read Framework Design Guidelines, so we don't have to worry whether or not the ApplicationSpecificException has the constructor we need.  I hope.

And the moral of the story is...

Without a proper stack trace, you can be absolutely lost in trying to debug an exception.  To keep your sanity and potentially your health (from getting thrashed by other developers), just follow a couple easy guidelines:

  • When re-throwing exceptions, use "throw;" instead of "throw ex;"
  • When wrapping exceptions, use a constructor that takes the inner exception

I didn't get into swallowing exceptions, that's an even worse nightmare.  I'll save that one for a future post.

For IL buffs

The difference between "throw;" and "throw ex;" is actually in the IL.  Two different IL instructions are used, "rethrow" and "throw" respectively.  This functionality isn't magic, it's built into the Framework at the IL level.

No comments: