C# – Proper Exception Handling

If buildings are built like software, some will occasionally crash due to architectural engineering mistakes or construction not meeting building codes. These mistakes are seen when buildings fail during earthquakes. Fortunately, most modern buildings are designed for the environment they reside in and inspected to ensure the design and construction adhere to building codes.

When an application crashes it typically is an inconvenience resulting in frustrated users, lost work, lost sales, or poor app reviews.

Application crashes reveal architectural and coding mistakes. Somewhere an unknown/unplanned event occurred that the application failed to handle and like a poorly designed building simply crashed.

Early in my career, I programmed embedded controllers and medical devices. Code reviews were thorough, so I read books on how minimize coding mistakes and how to handle exceptions. I continue to apply these techniques. The correct use of try/catch clauses do make applications resilient to most issues. However, while performing code reviews I have identified a total lack of exception handling or the incorrect use of. Like a thorough building inspection to certify a building is ready for habitation, a conscientious code review looking for proper handling of common software failures is essential for a resilient application.

Common manageable issues to be handled are intermittent network loss, network latency, long response time from a server and bad data from, databases or services.

One of my projects was a daemon that fetched data from a database, generated a report then wrote it to a network drive. Initial app testing revealed how intermittent the network was, extended response times from the database and poor performance of the file share. The network played a big part in the database and file share issues, which had their own issues. Database issues were addressed with code that said hello to determine if the database was available before making a query then waiting for a set period for the data to return. Requests were retried when the period expired. Availability of the network share was determined by making a call to check if the target directory exists, followed by an attempt to save the report file and finally a call to verify the file’s existence. If any step failed or an exception occurred the process to save the file was restarted. If either the database or file system did not respond, the report daemon would wait a period of time then check again. Each time there was no response the wait period was increased. After a set number of attempts were made, an email was sent to the support team notifying them of an extended outage and delay in report processing. Even thought the intermittent issues were common, a full outage was usually due to maintenance.

Reasons for Using a Try/Catch Clause

Not everything needs to be wrapped in a try/catch clause. Some exceptions are best left uncaught so that they can bubble up and handled at the proper level. Here are the only reasons to wrap code in a try/catch clause.

1. The catch will attempt to recover from the exception.  Perhaps your code is writing to a network file system which may disappear due to network instability.  Your catch could wait a period of time then retry the operation.

2. The exception is benign to the health and outcome of the application and can be discarded. The catch clause must have a comment stating that the exception is known and why it is harmless.

3. When adding information useful for debugging to the standard exception message. For instance, logging the user ID for a failed login to verify if the ID was entered correctly. When adding to an exception, the current exception must be included as an inner exception to maintain the stack trace.

4. The finally in a try/catch/finally does essential cleanup such as release unhandled memory, resources that could reduce performance or could cause resource contention.

Correctly Re-throwing an Exception

A common mistake I see when performing code reviews is incorrect use of try/catch clauses. Either the catch clause is empty so the exception is discarded and the application continues on and produces unexpected data results or the catch clause catches the exception then throws a new exception which replaces the stack trace so the error looks like it originated at the point of the new exception dead ending the debugging process. 
This sample program demonstrates the effect of catching and throwing an exception.  In a real application with thousands of lines of code, re-writing the call stack can lead to lengthy debug times as the real cause of the exception has been hidden.


  class Program
    {
        static void Main(string[] args)
        {
            var instance = new Program();
            try
            {
                System.Console.WriteLine("\n\n" + new String('-', 160));
                System.Console.WriteLine("Calling MethodA().  Stack trace shows exception originating in MethodA().");
                instance.MethodA();
            }
            catch (Exception ex)
            {
                LogException(ex, "Called MethodA");
            }
 
 
            try
            {
                System.Console.WriteLine("\n\n" + new String('-', 160));
                System.Console.WriteLine("MethodB() calls MethodA() but then throws a new exception, rewriting the stack trace and hiding the origin of the exception.  The exception appears to originate in Method(B) at throw ex.");
                instance.MethodB();
            }
            catch (Exception ex)
            {
                LogException(ex, "Called MethodB");
            }
 
 
            try
            {
                System.Console.WriteLine("\n\n" + new String('-', 160));
                System.Console.WriteLine("MethodC() calls MethodA() which simply re-throws the exception thrown by MethodA() therefore the try/catch clause has no effect and should be removed to improve readability and reduce overhead.");
                instance.MethodC();
            }
            catch (Exception ex)
            {
                LogException(ex, "Called MethodC");
            }
 
            System.Console.WriteLine("Press return to quit application.");
            System.Console.Read();
        }
 
 
        // This method just throws a NullReferenceException.
        public void MethodA()
        {
            throw new System.NullReferenceException("Oh No!  I haven't been instantiated.");
        }
 
 
        // MethodB() calls MethodA() but then throws a new exception, rewriting the stack trace and hiding the origin of the exception.  The exception appears to originate in Method(B) at throw ex.
        public void MethodB()
        {
            try
            {
                MethodA();
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }
 
 
        // MethodC() calls MethodA() which simply re-throws the exception thrown by MethodA() therefore the try/catch clause has no effect and should be removed to improve readability and reduce overhead.
        public void MethodC()
        {
            try
            {
                MethodA();
            }
            catch (Exception ex)
            {
                throw;
            }
        }
    }
MethodA() – A single level exception, not re-thrown

Stack trace shows exception originating in MethodA().

Output
Message: Called MethodA
Exception:
        Message: Oh No!  I haven't been instantiated.
        Stack Trace:
           at ExceptionHandlingExample.Program.MethodA() in C:\Users\jpbrockway\Source\Visual Studio Projects\ExceptionHandlingExample\Program.cs:line 59
   at ExceptionHandlingExample.Program.Main(String[] args) in C:\Users\jpbrockway\Source\Visual Studio Projects\ExceptionHandlingExample\Program.cs:line 18
MethodB() – A re-thrown exception resulting in a new stack trace

MethodB() calls MethodA() but then throws a new exception, rewriting the stack trace and hiding the origin of the exception.  The exception appears to originate in Method(B) at throw ex.

Output
Message: Called MethodB
Exception:
        Message: Oh No!  I haven't been instantiated.
        Stack Trace:
           at ExceptionHandlingExample.Program.MethodB() in C:\Users\jpbrockway\Source\Visual Studio Projects\ExceptionHandlingExample\Program.cs:line 74
   at ExceptionHandlingExample.Program.Main(String[] args) in C:\Users\jpbrockway\Source\Visual Studio Projects\ExceptionHandlingExample\Program.cs:line 30
MethodC() – A re-thrown exception preserving the stack trace

MethodC() calls MethodA() then simply re-throws the exception thrown by MethodA(), therefore the try/catch clause has no effect and should be removed to improve readability and reduce overhead.

Output
Message: Called MethodC
Exception:
        Message: Oh No!  I haven't been instantiated.
        Stack Trace:
           at ExceptionHandlingExample.Program.MethodA() in C:\Users\jpbrockway\Source\Visual Studio Projects\ExceptionHandlingExample\Program.cs:line 59
   at ExceptionHandlingExample.Program.MethodC() in C:\Users\jpbrockway\Source\Visual Studio Projects\ExceptionHandlingExample\Program.cs:line 90
   at ExceptionHandlingExample.Program.Main(String[] args) in C:\Users\jpbrockway\Source\Visual Studio Projects\ExceptionHandlingExample\Program.cs:line 42

Adding Additional Info

If you have additional information that could help in debugging the cause of an exception, you can create a new exception and include the current exception as an inner exception.


throw new Exception(message: $"Could not create export directory({ destinationExportDirectory }).", innerException: ex);

Conclusion

Take advantage of exception handling to recover from common issues. If your app encounters bad data, it should flag the data and continue processing the good data. When the network, filesystem or SQL Server is slow or unresponsive your app can pause and wait for their return. The more your application does to be resilient, the less you or support staff will need to do. In the end everyone will be happier.