When reviewing some code today I noticed some code that catches an exception, does something with it and then explicitly throws it again. The code looked something like this:
try { // Do something that might cause an exception } catch (Exception ex) { // Some stuff throw ex; }
The problem with the above code is that when you throw the exception again the details about where the exception originated from are lost because the throw populates that part of the exception object. So the original details are replaced with the details about the current location in the code.
Consider the following program:
static void Main(string[] args) { try { A(); } catch (Exception ex) { Console.WriteLine(ex.Message); Console.WriteLine(ex.StackTrace); } Console.ReadLine(); } private static void A() { B(); } private static void B() { try { C(); } catch (Exception ex) { // I can do something Console.WriteLine("Method C() catches the exception and partly handles it"); Console.WriteLine(); throw ex; } } private static void C() { D(); } private static void D() { Exception ex = new Exception("This exception is thrown in D"); throw ex; }
The original exception is thrown by method D. The code in method B catches the exception and partly handles it, it then explicitly throws the original exception again. When the exception is finally caught in the Main method the call stack is truncated to method B. It can no longer see that method C and D were also called.
The output of the application is:
Method C() catches the exception and partly handles it This exception is thrown in D at ConsoleApplication1.Program.B() in d:DevelopmentConsoleApplication1Cons oleApplication1Program.cs:line 40 at ConsoleApplication1.Program.A() in d:DevelopmentConsoleApplication1Cons oleApplication1Program.cs:line 26 at ConsoleApplication1.Program.Main(String[] args) in d:DevelopmentConsoleA pplication1ConsoleApplication1Program.cs:line 14
As you can see, the stack trace only goes from the point of the second throw to the point that the exception is caught.
There are two correct solutions to this problem.
Solution 1: Wrapping the Exception
If you have additional information to add to the exception object you can create a brand new Exception and then put the original exception in as an inner exception like this:
try { // Do something that might cause an exception } catch(Exception ex) { // Some stuff Exception moreDetailedEx = new Exception("A message with more details", ex); throw moreDetailedEx; }
NOTE: Please use a specific exception class and not Exception this makes catching specific types of exception easier and more efficient as the compiler can put in place some optimisations for you over you catching the base Exception class and examining it. Iām using Exception here simply to make the example easier to read.
So, if we change our program above to create a new exception and wrap the old one in it it will now look like this:
static void Main(string[] args) { try { A(); } catch (Exception ex) { Console.WriteLine(ex.InnerException.Message); Console.WriteLine(ex.InnerException.StackTrace); Console.WriteLine(); Console.WriteLine(ex.Message); Console.WriteLine(ex.StackTrace); } Console.ReadLine(); } private static void A() { B(); } private static void B() { try { C(); } catch (Exception ex) { // I can do something Console.WriteLine("Method C() catches the exception and partly handles it"); Console.WriteLine(); Exception newEx = new Exception("This exception is thrown in B", ex); throw newEx; } } private static void C() { D(); } private static void D() { Exception ex = new Exception("This exception is thrown in D"); throw ex; }
I’ve added some extra bits to the Main method to show the InnerException details too. The output of the program now looks like this:
Method C() catches the exception and partly handles it This exception is thrown in D at ConsoleApplication1.Program.D() in d:DevelopmentConsoleApplication1Cons oleApplication1Program.cs:line 56 at ConsoleApplication1.Program.C() in d:DevelopmentConsoleApplication1Cons oleApplication1Program.cs:line 50 at ConsoleApplication1.Program.B() in d:DevelopmentConsoleApplication1Cons oleApplication1Program.cs:line 36 This exception is thrown in B at ConsoleApplication1.Program.B() in d:DevelopmentConsoleApplication1Cons oleApplication1Program.cs:line 44 at ConsoleApplication1.Program.A() in d:DevelopmentConsoleApplication1Cons oleApplication1Program.cs:line 29 at ConsoleApplication1.Program.Main(String[] args) in d:DevelopmentConsoleA pplication1ConsoleApplication1Program.cs:line 14
As you can now see all the information is available. It can now be seen the patch from the final point the exception was caught to the point it was originally thrown.
Solution 2: Re-throwing the Exception
If you do not have any additional information to add to the exception you can simply use the throw keyword on its own and it will keep the existing exception object without altering it. For example:
try { // Do something that might cause an exception } catch (Exception ex) { // Some stuff throw; }
Changing our program above to use the throw statement on its own will mean the program now looks like this:
static void Main(string[] args) { try { A(); } catch (Exception ex) { Console.WriteLine(ex.Message); Console.WriteLine(ex.StackTrace); } Console.ReadLine(); } private static void A() { B(); } private static void B() { try { C(); } catch (Exception ex) { // I can do something Console.WriteLine("Method C() catches the exception and partly handles it"); Console.WriteLine(); throw; } } private static void C() { D(); } private static void D() { Exception ex = new Exception("This exception is thrown in D"); throw ex; }
And the output now looks like this:
Method C() catches the exception and partly handles it This exception is thrown in D at ConsoleApplication1.Program.D() in d:DevelopmentConsoleApplication1Cons oleApplication1Program.cs:line 52 at ConsoleApplication1.Program.C() in d:DevelopmentConsoleApplication1Cons oleApplication1Program.cs:line 46 at ConsoleApplication1.Program.B() in d:DevelopmentConsoleApplication1Cons oleApplication1Program.cs:line 40 at ConsoleApplication1.Program.A() in d:DevelopmentConsoleApplication1Cons oleApplication1Program.cs:line 26 at ConsoleApplication1.Program.Main(String[] args) in d:DevelopmentConsoleA pplication1ConsoleApplication1Program.cs:line 14
As you can see the stack trace now shows you the entire route between the point the exception was caught and when it was thrown. We can also see from the message that the method C still caught and partly handled the exception.
Using the throw keyword like this actually translates to the CIL (MSIL) rethrow command. When you use throw with an Exception object it translates to the CIL throw command.
When the exception is thrown in D the CIL looks like this:
.method private hidebysig static void D() cil managed { .maxstack 2 .locals init ( [0] class [mscorlib]System.Exception ex) L_0000: nop L_0001: ldstr "This exception is thrown in D" L_0006: newobj instance void [mscorlib]System.Exception::.ctor(string) L_000b: stloc.0 L_000c: ldloc.0 L_000d: throw }
The key above is L_000d where it throws the exception.
Compare that to method B:
.method private hidebysig static void B() cil managed { .maxstack 1 .locals init ( [0] class [mscorlib]System.Exception ex) L_0000: nop L_0001: nop L_0002: call void ConsoleApplication1.Program::C() L_0007: nop L_0008: nop L_0009: leave.s L_0020 L_000b: stloc.0 L_000c: nop L_000d: ldstr "Method C() catches the exception and partly handles it" L_0012: call void [mscorlib]System.Console::WriteLine(string) L_0017: nop L_0018: call void [mscorlib]System.Console::WriteLine() L_001d: nop L_001e: rethrow L_0020: nop L_0021: ret .try L_0001 to L_000b catch [mscorlib]System.Exception handler L_000b to L_0020 }
In the above CIL code the key is L_001e where it rethrows the exception. This is where the CIL is much more explicit than C#. In C# the throw keyword is overloaded and functions differently depending on whether it receives an exception object or not.
Excellent! Many was the time that I told young[er] developers of the folly of naively re-throwing exceptions. Yours is an excellent explanation with very clear solutions. Expected of you Colin š
Never realised this. As a beginning c# programmer i thank you š