Sunday, March 12, 2006 9:46 PM
bart
To throw or to rethrow?
It has been a long time since my last technical post on my blog over here. Let's do something about it. A couple of days ago I was made aware of some strange behavior on the field of exception handling in the .NET Framework. Although a little surprised in the beginning, I took the tools in hand and started to investigate the following behavior.
What will be the output of the following?
using System;
class Ex
{
public static void Main()
{
//
// First test rethrowing the caught exception variable.
//
Console.WriteLine("First test");
try
{
ThrowWithVariable();
}
catch (Exception ex)
{
Console.WriteLine(ex.StackTrace);
}
//
// Second test performing a blind rethrow.
//
Console.WriteLine("Second test");
try
{
ThrowWithoutVariable();
}
catch (Exception ex)
{
Console.WriteLine(ex.StackTrace);
}
}
private static void BadGuy()
{
//
// Some nasty behavior.
//
throw new Exception();
}
private static void ThrowWithVariable()
{
try
{
BadGuy();
}
catch (Exception ex)
{
throw ex;
}
}
private static void ThrowWithoutVariable()
{
try
{
BadGuy();
}
catch
{
throw;
}
}
}
This piece of code uses "exception rethrow" in two different ways. The first test rethrows the exception object received through the catch block parameter, whileas the second one performs kind of a blind rethrow just using the keyword "throw". I'm not going to cover the scenarios where rethrowing an exception is a good design choice over here (e.g. from a performance point of view you should try to avoid rethrowing exceptions). Nevertheless, the behavior is different in both cases. Here's the output:
First test
at Ex.ThrowWithVariable()
at Ex.Main()
Second test
at Ex.BadGuy()
at Ex.ThrowWithoutVariable()
at Ex.Main()
Oops, our bad guy has disappeared in the first test, but why?
Sniffing a line of IL
Time for ildasm.exe once again. Regular blog readers will know ildasm.exe is still one of my favorite tools (with reason in my opinion). Here's the code for the ThrowWithVariable method:
.method private hidebysig static void ThrowWithVariable() cil managed
{
// Code size 17 (0x11)
.maxstack 1
.locals init (class [mscorlib]System.Exception V_0)
IL_0000: nop
.try
{
IL_0001: nop
IL_0002: call void Ex::BadGuy()
IL_0007: nop
IL_0008: nop
IL_0009: leave.s IL_000f
} // end .try
catch [mscorlib]System.Exception
{
IL_000b: stloc.0
IL_000c: nop
IL_000d: ldloc.0
IL_000e: throw
} // end handler
IL_000f: nop
IL_0010: ret
} // end of method Ex::ThrowWithVariable
Ignore the nop statements due to the debugging build. As you can see, the exception being thrown over here is grabbed from the locals (V_0). However, the second test contains different code:
.method private hidebysig static void ThrowWithoutVariable() cil managed
{
// Code size 17 (0x11)
.maxstack 1
IL_0000: nop
.try
{
IL_0001: nop
IL_0002: call void Ex::BadGuy()
IL_0007: nop
IL_0008: nop
IL_0009: leave.s IL_000f
} // end .try
catch [mscorlib]System.Object
{
IL_000b: pop
IL_000c: nop
IL_000d: rethrow
} // end handler
IL_000f: nop
IL_0010: ret
} // end of method Ex::ThrowWithoutVariable
Over here, the caught exception s grabbed from the stack and rethrown directly. Because of this, we keep the existing information of the original exception and reuse it. To understand the overall behavior of the exception handler and the relation to the stack, it's sufficient to understand that the exception object is living on top of the stack when entering the catch block. Also notice that our "catch all" was translated to a catch block handling objects of the type System.Object (no, System.Exception should not be the parent of all exception types in .NET, that's just a rule which is enforced by the compilers).
Conclusion
A "blind" rethrow is the best way to rethrow an exception because it maintains all the information about the real origin of the exception. Don't get caught by this behavior :-).
UPDATE: Robert (see comments) points out that a blind rethrow isn't a good idea. I agree on this point, but this conclusion is not dealing with the "philosophy of exception handling". Instead, this conclusion is only based on the comparison between "throw" and "throw ex". Whether doing such a thing without wrapping the exception and adding additional information is a good idea, isn't the scope of this post. Check out Brad Adams' PPT on this subject for more information.
Del.icio.us |
Digg It |
Technorati |
Blinklist |
Furl |
reddit |
DotNetKicks
Filed under: .NET Framework v2.0, C# 2.0