This is the second part of the .NET Inside Out series. For your convenience you can find other parts in the table of contents in Part 1 – Virtual and non-virtual calls in C#
Today we are going to dive into exceptions in .NET world. Handling them right might be a bit tricky, especially when you do not understand internals.
Most of this post is based on .NET Framework. Scroll down to .NET Core updated part.
Table of Contents
Exceptions at a glance
I assume you know what is an exception, when it should be used, how to throw it, and how to catch it. You can also read about exception internals
Rethrowing exceptions
We know how to throw and catch exception. Imagine now, that we want to catch exception, examine it and do something (e.g., log its message), and then rethrow it. Let’s take the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
01. using System; 02. using System.Threading; 03. 04. namespace ExceptionTests 05. { 06. class Program 07. { 08. public static void MethodThrowingException() 09. { 10. throw new ArgumentException("Hold on!"); 11. } 12. 13. public static void MethodRethrowingException() 14. { 15. try 16. { 17. MethodThrowingException(); 18. MethodThrowingException(); 19. } 20. catch (Exception e) 21. { 22. if (e.Message == "Custom message") 23. { 24. Console.WriteLine("Custom exception"); 25. } 26. 27. // Rethrow it here 28. } 29. } 30. 31. static void Main() 32. { 33. Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("en-US"); 34. MethodRethrowingException(); 35. } 36. } 37. } |
We have method named MethodThrowingException
which throws exception of type ArgumentException
. Next, we have method MethodRethrowingException
which calls MethodThrowingException
twice, handles exception and rethrows it. Finally, in Main
we first set culture to English (in order to have readable exception descriptions), and next we call method handling exceptions.
Two things are worth noting here: we call method MethodThrowingException
twice (even though it always throws exception and second invocation never happens). We also check the message of the exception when handling it, but it never meets condition so we always rethrow it without doing anything.
Since Main
does not handle exceptions, they will be printed out to the console (with all inner exceptions), and the program will crash. Let’s now examine multiple methods of rethrowing exception.
Rethrowing by throwing the same exception again
We can use the following line:
1 |
throw e; |
It throws existing exception. Let’s see it in action, this is the program output:
1 2 3 |
Unhandled Exception: System.Exception: Hold on! at ExceptionTests.Program.MethodRethrowingException() in c:\users\user\documents\visual studio 2015\Projects\ConsoleApplication2\ConsoleApplication2\Program.cs:line 27 at ExceptionTests.Program.Main() in c:\users\user\documents\visual studio 2015\Projects\ConsoleApplication2\ConsoleApplication2\Program.cs:line 34 |
So what did we get here? Our stacktrace contains only information related to last throw. We lost all informations about the exception being thrown in the first place.
Never rethrow exception this way!
Let’s now see the stacktrace visible by the debugger:
1 2 3 4 5 6 7 8 |
0:000> !clrstack OS Thread Id: 0x4bac (0) Child SP IP Call Site 00f3ee4c 755bdae8 [HelperMethodFrame: 00f3ee4c] 00f3eefc 01580537 *** WARNING: Unable to verify checksum for ConsoleApplication2.exe ExceptionTests.Program.MethodRethrowingException() [c:\users\user\documents\visual studio 2015\Projects\ConsoleApplication2\ConsoleApplication2\Program.cs @ 29] 00f3eff0 0158049c ExceptionTests.Program.Main() [c:\users\user\documents\visual studio 2015\Projects\ConsoleApplication2\ConsoleApplication2\Program.cs @ 34] 00f3f170 70131376 [GCFrame: 00f3f170] |
We can’t see original frame of the method which was first to throw.
Rethrowing by throw;
Most of the C# books says that the right way to rethrow exception is the instruction throw;
. Let’s see it in action:
1 2 3 4 |
Unhandled Exception: System.ArgumentException: Hold on! at ExceptionTests.Program.MethodThrowingException() in c:\users\user\documents\visual studio 2015\Projects\ConsoleApplication2\ConsoleApplication2\Program.cs:line 10 at ExceptionTests.Program.MethodRethrowingException() in c:\users\user\documents\visual studio 2015\Projects\ConsoleApplication2\ConsoleApplication2\Program.cs:line 27 at ExceptionTests.Program.Main() in c:\users\user\documents\visual studio 2015\Projects\ConsoleApplication2\ConsoleApplication2\Program.cs:line 34 |
Indeed, this time we have line which originally thrown the exception. Let’s see WinDBG stacktrace:
1 2 3 4 5 6 7 8 |
0:000> !clrstack OS Thread Id: 0xff0 (0) Child SP IP Call Site 00b7f11c 755bdae8 [HelperMethodFrame: 00b7f11c] 00b7f18c 02910534 *** WARNING: Unable to verify checksum for ConsoleApplication2.exe ExceptionTests.Program.MethodRethrowingException() [c:\users\user\documents\visual studio 2015\Projects\ConsoleApplication2\ConsoleApplication2\Program.cs @ 29] 00b7f280 0291049c ExceptionTests.Program.Main() [c:\users\user\documents\visual studio 2015\Projects\ConsoleApplication2\ConsoleApplication2\Program.cs @ 34] 00b7f3f8 70131376 [GCFrame: 00b7f3f8] |
Unfortunately, stacktrace didn’t change. We still can’t see the datils. But we have them in our exception object, so we can extract them anyway, can’t we?
Well, the answer is: not really. Are you able to tell which invocation of MethodThrowingException
thrown exception? Remember that we are calling this method twice but there is no clue in stacktrace or exception which invocation was the one to blame. We could try to analyze the code but we have no stack parameters anymore, so it is not always sufficient.
Most books describe throw;
as a proper way to rethrow exception. But it is not enough!
So how should we rethrow exception?
Create new exception and throw it
Let’s now rethrow the exception with the following line:
1 |
throw new Exception(e.Message, e) |
Output:
1 2 3 4 5 6 |
Unhandled Exception: System.Exception: Hold on! ---> System.ArgumentException: Hold on! at ExceptionTests.Program.MethodThrowingException() in c:\users\user\documents\visual studio 2015\Projects\ConsoleApplication2\ConsoleApplication2\Program.cs:line 10 at ExceptionTests.Program.MethodRethrowingException() in c:\users\user\documents\visual studio 2015\Projects\ConsoleApplication2\ConsoleApplication2\Program.cs:line 17 --- End of inner exception stack trace --- at ExceptionTests.Program.MethodRethrowingException() in c:\users\user\documents\visual studio 2015\Projects\ConsoleApplication2\ConsoleApplication2\Program.cs:line 27 at ExceptionTests.Program.Main() in c:\users\user\documents\visual studio 2015\Projects\ConsoleApplication2\ConsoleApplication2\Program.cs:line 34 |
This time we finally have all informations. Let’s see the WinDBG stacktrace:
1 2 3 4 5 6 7 8 |
0:000> !clrstack OS Thread Id: 0x470 (0) Child SP IP Call Site 004ff144 755bdae8 [HelperMethodFrame: 004ff144] 004ff1f4 00760560 *** WARNING: Unable to verify checksum for ConsoleApplication2.exe ExceptionTests.Program.MethodRethrowingException() [c:\users\user\documents\visual studio 2015\Projects\ConsoleApplication2\ConsoleApplication2\Program.cs @ 29] 004ff2f0 0076049c ExceptionTests.Program.Main() [c:\users\user\documents\visual studio 2015\Projects\ConsoleApplication2\ConsoleApplication2\Program.cs @ 34] 004ff468 70131376 [GCFrame: 004ff468] |
Unfortunately, stacktrace is still the same.
But! This changes the type of the exception which can change the logc of your code badly. SO you should not do it just like that, you should probably use reflection to create exception of the same type (which might no be possible though).
PrepForRemoting
Milosz Krajewski suggested yet another way. There is an internal method PrepForRemoting
which sets the field _remoteStackTraceString
. If you call it the stacktrace is preserved:
1 2 |
typeof(Exception).GetMethod("PrepForRemoting", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(e, new object[0]); throw e; |
Output:
1 2 3 4 5 6 7 |
Server stack trace: at ExceptionTests.Program.MethodThrowingException() in c:\users\user\documents\visual studio 2015\Projects\ConsoleApplication2\ConsoleApplication2\Program.cs:line 10 at ExceptionTests.Program.MethodRethrowingException() in c:\users\user\documents\visual studio 2015\Projects\ConsoleApplication2\ConsoleApplication2\Program.cs:line 17 Exception rethrown at [0]: at ExceptionTests.Program.MethodRethrowingException() in c:\users\user\documents\visual studio 2015\Projects\ConsoleApplication2\ConsoleApplication2\Program.cs:line 28 at ExceptionTests.Program.Main() in c:\users\user\documents\visual studio 2015\Projects\ConsoleApplication2\ConsoleApplication2\Program.cs:line 35 |
ExceptionDispatchInfo
.NET 4.5 introduced class ExceptionDispatchInfo which can capture exception and rethrow it somewhere else without spoiling the stack trace. Let’s see it in action:
1 |
ExceptionDispatchInfo.Capture(e).Throw(); |
Output:
1 2 3 4 5 6 7 |
Unhandled Exception: System.Exception: Hold on! at ExceptionTests.Program.MethodThrowingException() in c:\users\user\documents\visual studio 2015\Projects\ConsoleApplication2\ConsoleApplication2\Program.cs:line 10 at ExceptionTests.Program.MethodRethrowingException() in c:\users\user\documents\visual studio 2015\Projects\ConsoleApplication2\ConsoleApplication2\Program.cs:line 17 --- End of stack trace from previous location where exception was thrown --- at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at ExceptionTests.Program.MethodRethrowingException() in c:\users\user\documents\visual studio 2015\Projects\ConsoleApplication2\ConsoleApplication2\Program.cs:line 27 at ExceptionTests.Program.Main() in c:\users\user\documents\visual studio 2015\Projects\ConsoleApplication2\ConsoleApplication2\Program.cs:line 34 |
And now WinDBG callstack:
1 2 3 4 5 6 7 |
0:000> !clrstack OS Thread Id: 0x470 (0) Child SP IP Call Site 004ff144 755bdae8 [HelperMethodFrame: 004ff144] 010ff3e8 016d0544 *** WARNING: Unable to verify checksum for ConsoleApplication2.exe ExceptionTests.Program.MethodRethrowingException() [C:\Users\user\Documents\Visual Studio 2015\Projects\ConsoleApplication2\ConsoleApplication2\Program.cs @ 27] 010ff4e0 016d049c ExceptionTests.Program.Main() [C:\Users\user\Documents\Visual Studio 2015\Projects\ConsoleApplication2\ConsoleApplication2\Program.cs @ 34] |
It looks like nothing changed.
So what is the right way to rethrow exception? It looks like PrepForRemoting is the best. It works on old .NET Framework and contains most of useful data. If in .NET 4.5, just use new method.
Examining exception
Our exception handler does something interesting only when the message is set to correct value. Most of the times this is not the case so it does nothing. Is there a better approach to perform this message check?
C# 6 introduced filtering statement for exception handlers. Let’s see them in action:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
01. using System; 02. using System.Threading; 03. 04. namespace ExceptionTests 05. { 06. class Program 07. { 08. public static void MethodThrowingException() 09. { 10. throw new Exception("Hold on!"); 11. } 12. 13. public static void MethodRethrowingException() 14. { 15. try 16. { 17. MethodThrowingException(); 18. MethodThrowingException(); 19. } 20. catch (Exception e) when (e.Message == "Custom message") 21. { 22. Console.WriteLine("Custom exception"); 23. 24. // Rethrow 25. } 26. } 27. 28. static void Main() 29. { 30. Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("en-US"); 31. MethodRethrowingException(); 32. } 33. } 34. } |
So we moved our condition to special filter outside of the exception handler. Does it change anything? If you run this application with different rethrowing instructions, the output will be different:
1 2 3 4 |
Unhandled Exception: System.Exception: Hold on! at ExceptionTests.Program.MethodThrowingException() in c:\users\user\documents\visual studio 2015\Projects\ConsoleApplication2\ConsoleApplication2\Program.cs:line 10 at ExceptionTests.Program.MethodRethrowingException() in c:\users\user\documents\visual studio 2015\Projects\ConsoleApplication2\ConsoleApplication2\Program.cs:line 17 at ExceptionTests.Program.Main() in c:\users\user\documents\visual studio 2015\Projects\ConsoleApplication2\ConsoleApplication2\Program.cs:line 31 |
There is no surprise here: since our exception handler didn’t execute, we didn’t need to rethrow exception, so we get original stacktrace regardless the rethrowing method. Let’s now see the WinDBG:
1 2 3 4 5 6 7 8 9 |
0:000> !clrstack OS Thread Id: 0x5d18 (0) Child SP IP Call Site 008ff3cc 755bdae8 [HelperMethodFrame: 008ff3cc] 008ff47c 00ab0607 *** WARNING: Unable to verify checksum for ConsoleApplication2.exe ExceptionTests.Program.MethodThrowingException() [c:\users\user\documents\visual studio 2015\Projects\ConsoleApplication2\ConsoleApplication2\Program.cs @ 9] 008ff48c 00ab04ed ExceptionTests.Program.MethodRethrowingException() [c:\users\user\documents\visual studio 2015\Projects\ConsoleApplication2\ConsoleApplication2\Program.cs @ 17] 008ff4d0 00ab049c ExceptionTests.Program.Main() [c:\users\user\documents\visual studio 2015\Projects\ConsoleApplication2\ConsoleApplication2\Program.cs @ 31] 008ff648 70131376 [GCFrame: 008ff648] |
This is very important. New syntax allows us to not execute the exception handler when the condition is not met. When it is the case, the stacktrace does not change!
Conclusion
Most of C# books describes throw;
instruction as the way to go. However, be very careful with it since it loses some pieces of information. It might look like not interesting edge case (why do you want to call particular function multiple times?), but you should be aware of this behaviour. Also, always try to use new syntax for filtering exceptions since it doesn’t modify call stack. One day it might save your life.
Update: .NET Core
Things have changed in .NET Core. While first method (throw e
) still doesn’t print all lines in the output, WinDBG is capable of seeing it correctly:
1 2 3 4 5 6 7 8 9 10 11 |
0:000> !clrstack OS Thread Id: 0x2288 (0) Child SP IP Call Site 00000007D237BEE8 00007ffcd32ea839 [HelperMethodFrame: 00000007d237bee8] 00000007D237BFE0 00007ffc2af012ad RethrowingExceptionNetCore.Program.RethrowException() [C:\Users\afish\Desktop\msp_windowsinternals\workshops\RethrowingExceptionNetCore\Program.cs @ 25] 00000007D237E268 00007ffc8a97bf8f [HelperMethodFrame: 00000007d237e268] 00000007D237E360 00007ffc2af01344 RethrowingExceptionNetCore.Program.ThrowException() [C:\Users\afish\Desktop\msp_windowsinternals\workshops\RethrowingExceptionNetCore\Program.cs @ 8] 00000007D237E3A0 00007ffc2af01234 RethrowingExceptionNetCore.Program.RethrowException() [C:\Users\afish\Desktop\msp_windowsinternals\workshops\RethrowingExceptionNetCore\Program.cs @ 16] 00000007D237E400 00007ffc2af011e2 RethrowingExceptionNetCore.Program.Main(System.String[]) [C:\Users\afish\Desktop\msp_windowsinternals\workshops\RethrowingExceptionNetCore\Program.cs @ 30] 00000007D237E618 00007ffc8aa065f3 [GCFrame: 00000007d237e618] 00000007D237EBC0 00007ffc8aa065f3 [GCFrame: 00000007d237ebc0] |
Second method has the same output in WinDBG and slightly better output in the console:
1 2 3 4 |
Unhandled exception. System.InvalidOperationException: Invalid operation at RethrowingExceptionNetCore.Program.ThrowException() in C:\Users\afish\Desktop\msp_windowsinternals\workshops\RethrowingExceptionNetCore\Program.cs:line 9 at RethrowingExceptionNetCore.Program.RethrowException() in C:\Users\afish\Desktop\msp_windowsinternals\workshops\RethrowingExceptionNetCore\Program.cs:line 16 at RethrowingExceptionNetCore.Program.Main(String[] args) in C:\Users\afish\Desktop\msp_windowsinternals\workshops\RethrowingExceptionNetCore\Program.cs:line 30 |
So we have important lines for the exception but we don’t see that it was rethrown.
Finally, last method based on async. Output:
1 2 3 4 5 6 |
Unhandled exception. System.InvalidOperationException: Invalid operation at RethrowingExceptionNetCore.Program.ThrowException() in C:\Users\afish\Desktop\msp_windowsinternals\workshops\RethrowingExceptionNetCore\Program.cs:line 9 at RethrowingExceptionNetCore.Program.RethrowException() in C:\Users\afish\Desktop\msp_windowsinternals\workshops\RethrowingExceptionNetCore\Program.cs:line 16 --- End of stack trace from previous location where exception was thrown --- at RethrowingExceptionNetCore.Program.RethrowException() in C:\Users\afish\Desktop\msp_windowsinternals\workshops\RethrowingExceptionNetCore\Program.cs:line 23 at RethrowingExceptionNetCore.Program.Main(String[] args) in C:\Users\afish\Desktop\msp_windowsinternals\workshops\RethrowingExceptionNetCore\Program.cs:line 30 |
All lines. WinDBG sees this:
1 2 3 4 5 6 7 8 9 10 |
0:000> !clrstack OS Thread Id: 0x1780 (0) Child SP IP Call Site 00000057C2D7C4B8 00007ffa8945a839 [HelperMethodFrame: 00000057c2d7c4b8] 00000057C2D7C5B0 00007ffa41e95571 System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() [/_/src/System.Private.CoreLib/shared/System/Runtime/ExceptionServices/ExceptionDispatchInfo.cs @ 62] 00000057C2D7C5E0 00007ff9e6fa12bb RethrowingExceptionNetCore.Program.RethrowException() [C:\Users\afish\Desktop\msp_windowsinternals\workshops\RethrowingExceptionNetCore\Program.cs @ 23] 00000057C2D7E888 00007ffa46a2bf8f [HelperMethodFrame: 00000057c2d7e888] 00000057C2D7E980 00007ff9e6fa1364 RethrowingExceptionNetCore.Program.ThrowException() [C:\Users\afish\Desktop\msp_windowsinternals\workshops\RethrowingExceptionNetCore\Program.cs @ 8] 00000057C2D7E9C0 00007ff9e6fa1231 RethrowingExceptionNetCore.Program.RethrowException() [C:\Users\afish\Desktop\msp_windowsinternals\workshops\RethrowingExceptionNetCore\Program.cs @ 16] 00000057C2D7EA30 00007ff9e6fa11e2 RethrowingExceptionNetCore.Program.Main(System.String[]) [C:\Users\afish\Desktop\msp_windowsinternals\workshops\RethrowingExceptionNetCore\Program.cs @ 30] |
So it looks like solution ExceptionDispatchInfo
is still the best.