One of the more frustrating things to deal with when supporting a production application is “flying blind” when trying to determine what went wrong and how to fix it. In today’s post, I will discuss some commonly used techniques for handling exceptions in .NET code that prevent visibility into the real source of error and demonstrate alternative approaches to preserve and enhance exception information.
The most basic tool for dealing with exceptions in a .NET application is the try/catch construct. This construct allows the developer to specify that if an exception occurs within a specific block of code control should be transfered to another block along with information about the exception that occured. The form of a try/catch block is as follows:
try { // some code that errors out } catch(Exception e) { // handle the exception }
One of the most common mis-uses of a try/catch block that I have observed is the tendency of developers to “pass the exception up” to calling code in a manner similar to the following code:
try { return operand1 / operand2; } catch(Exception e) { throw new Exception(e.Message); }
For the sake of this posting we will ignore the performance implications of generating new exceptions and focus solely on the impact that this has to problem determination and resolution. The problem with this approach is that the source line at which the exception is now reported is the line containing the throw statement and all contextual information (including the actual exception type) is lost. To put a little bit of context around it, consider the following code:
static void Main(string[] args) { try { Console.WriteLine(Divide(0, 0)); } catch (Exception e) { Console.WriteLine("Exception Caught:"); Console.WriteLine(e.ToString()); } Console.ReadLine(); } static int Divide(int operand1, int operand2) { try { return InnerDivide(0,0); } catch(Exception e) { throw new Exception(e.Message); } } static int InnerDivide(int operand1, int operand2) { try { return operand1 / operand2; } catch(Exception e) { throw new Exception(e.Message); } }
When executed, the code produces the following output:
Exception Caught: System.Exception: Attempted to divide by zero. at ExceptionDemo.Program.Divide(Int32 operand1, Int32 operand2) in c:\...\Program.cs:line 35 at ExceptionDemo.Program.Main(String[] args) in c:\...\Program.cs:line 15
Looking at the stack trace, you can see that the source of the exception is listed as within the Divide method even though the actual error occurs within the InnerDivide method. Also note that even though a very specific DivideByZeroException was originally thrown, a generic System.Exception is what is ultimately reported. In a more complex system, this could have support engineers examining the wrong parts of the code and extend the time required to find the real problem.
Another variation of the same pattern comes when the developer attempts pass the original exception rather than creating a new one. This often results in code such as the following:
try { // code producing exception } catch(Exception e) { throw e; }
When the sample code above is update to use this technique, it produces the following output:
Exception Caught: System.DivideByZeroException: Attempted to divide by zero. at ExceptionDemo.Program.Divide(Int32 operand1, Int32 operand2) in c:\...\Program.cs:line 35 at ExceptionDemo.Program.Main(String[] args) in c:\...\Program.cs:line 15
This is slightly better than the previous output in that the original exception type is preserved, but it still carries the disadvantage of losing the original source of the exception within the stack trace. This is because of the way that C# compiles to MSIL. MSIL contains two methods of propagating exception information. The first is the “throw” statement which is used for a new exception and the second is the “rethrow” statement which is used to allow an existing exception to continue bubbling up through the call stack. When C# compiles to MSIL, passing an exception to the throw statement causes the MSIL “throw” statement to be generated while using the throw statement by itself in a line causes the rethrow statement to be generated. The form of try/catch statement which allows the original exception to remain intact is as follows:
try { // code that generates exception } catch(Exception e) { throw; }
When the sample program is updated to use this technique, the output is as follows:
Exception Caught: System.DivideByZeroException: Attempted to divide by zero. at ExceptionDemo.Program.InnerDivide(Int32 operand1, Int32 operand2) in c:\...\Program.cs:line 47 at ExceptionDemo.Program.Divide(Int32 operand1, Int32 operand2) in c:\...\Program.cs:line 35 at ExceptionDemo.Program.Main(String[] args) in c:\...\Program.cs:line 15
Now the actual line at which the exception occured is communicated along with all of its data being intact.
It’s important to note that throughout the demonstration of the most common problem that I see with how developers (mis)use exceptions, I have quietly introduced the second most common problem – that is having exception handling code that adds no value. It’s common for organizations to have coding standards (documented or otherwise) that dictate every method should use try/catch blocks. In these organizations we often find the “catch and throw” construct that was used in the code samples. In these scenarios we add lines of code and reduce readability without adding any value by performing activities such as logging or adding contextual data to the exception and it would be much better not to have try/catch blocks at all. The sample program used earlier provides identical output and is much easier to read when updated as follows:
static void Main(string[] args) { try { Console.WriteLine(Divide(0, 0)); } catch (Exception e) { Console.WriteLine("Exception Caught:"); Console.WriteLine(e.ToString()); } Console.ReadLine(); } static int Divide(int operand1, int operand2) { return InnerDivide(0,0); } static int InnerDivide(int operand1, int operand2) { return operand1 / operand2; }
There are many ways to make sure that application failures in production can be made easier to diagnose and resolve and today we have discussed only how a change in approach to the way that try/catch blocks are used can help. I my next posting, we’ll look at another way to provide runtime information to whoever is supporting your application.
Awesome Post!