Sinara_Pos_DIGITAL

Unhandled Exceptions in .NET

At Sinara, we build many server-side applications that must run 24×7 and cope successfully with the myriad problems that might occur in any live production system, including network outages, disk space problems, server restarts, slow connections, etc. We therefore devote a great deal of development time to ensuring that the code we build is resilient and can correctly recover from many common runtime conditions.

The basic mechanism in most modern programming languages for catching and handling errors is through the use of exceptions. In this article, we examine various issues surrounding how exceptions are handled in .NET.

Runtime error conditions vs program logic errors

First, a quick recap. Before exception handling became widespread, error checking was mainly done through the use of error codes. Each method would return an enum value or integer that indicated whether the operation had been successful or whether a particular kind of error had occurred. It wasn’t uncommon to provide additional helper methods to return error messages, further details, etc. If you were being more object-oriented about it, you could return an error/status object that encapsulated all this information. (By the way, this still isn’t a bad idea and can be very useful in some cases).

However, the key problem was that you were forced to explicitly check the error code after each call in order to make sure that it had been successful before proceeding. This meant the code would typically include lots of if-statements that could obscure the flow of the algorithm and make it harder to maintain.

In spite of this tediousness, one arguable benefit of using error codes was that it forced the developer to explicitly check for error conditions and to structure the code in such a way that these were correctly handled. Not checking the return codes would be bad programming to say the least!

Whilst exceptions remove the need for lots of if-statements, they can potentially make it harder to distinguish between external error conditions at runtime that can be dealt with (connection failed, file not found, etc), and errors within the program logic that cannot (null reference, index out of range, etc). In C++, the former raises an exception (or returns an error code depending on the API), whilst the latter will cause the program to terminate. In .NET, both types of error will raise an exception.

C++  too is gradually moving towards the .NET way. For example, smart pointers will throw an exception if the underlying pointer is null.

These two types of exception should clearly be handled differently. Runtime error conditions should be recovered from (retry, inform the user, etc), whilst bugs in the code need to be recorded and the program either gracefully degraded or shut down.

Catch-all

It is often the case that developers will simply place a ‘catch-all’ on each thread, method or even code block to ensure that all types of exceptions are caught and logged (followed perhaps by a delay and retry):

try
{
 ...
}
catch (Exception ex)
{
 Trace.TraceInformation(ex.ToString());
 ...
}

This problem is that this defeats the purpose of exception handling, which is to catch certain types of exception that we specifically know how to handle. For example, when working with a SQL Server database, we can expect a SqlException to occur if, for example, the database is unavailable. It makes sense to catch this exception, log it, perhaps add a delay, and retry later. We could also check the SQL error code to see if it is worth retrying or just discarding the data, if appropriate.

Another problem with the above code is that it generates a full stack trace within the logged message, which isn’t typically needed for regular errors such as disconnections. Having unnecessary stack traces in a log file can give the impression that something has gone badly wrong within the program when it actually hasn’t.

But does it also make sense, as will happen in the above code block, to also catch a NullReferenceException or IndexOutOfRangeException during the database operation and log the error in the same way? Such an exception would normally be a result of something going wrong in the code that we didn’t consider beforehand. Perhaps we never expected a database field to be null and so didn’t add the logic to handle it. Retrying would just raise the same exception. On the other hand, the problem (in this case) might be fixed by changing the data, enabling a temporary workaround. This would depend on the specific problem–a workaround wouldn’t always be possible.

Unhandled exceptions

So if we only add code to catch specific exceptions, what should we do with the ones that are unhandled?

In theory, it is best to let the OS terminate the process. The system will create a memory dump and log the event, making it clear that something went wrong. In .NET, a stack trace will also be generated indicating whereabouts in the code the exception was raised.

In practice, we would usually prefer that programs didn’t crash in that way, and that they terminate in as graceful a manner as possible, with minimal data loss. In some cases, we might even prefer for the program to continue in a reduced functional state until it can be manually restarted.

This is feasible to a degree, depending on the program itself. In any case, at minimum, it is useful to have the error logged in the program’s standard log file with a full stack trace to aid with debugging. Once this is done, the program can then shut down or enter a reduced operational state.

Unhandled exceptions in Threads

An unhandled exception in the main program or in a Thread callback will result in the program being immediately terminated.

If it is acceptable for the thread to shut down without the entire program shutting down, you could add a top-level catch (Exception) block to the thread callback:

Thread t = new Thread(() =>
{
 try
 {
  ...
 }
 catch (Exception ex)
 {
  SaveData();
  DegradeGracefully();
  Trace.TraceInformation("Unhandled exception: {0}.", ex);
 }
});

Note that this catch-all is a backstop for errors in the code, not the primary exception-handling mechanism. You would still add individual try/catch blocks within the methods called on the thread to handle specific error conditions. The catch-all simply ensures that the program doesn’t crash.

Adding a global unhandled exception ‘handler’

It is also possible to capture an unhandled exception thrown on any thread within a program by adding an event handler to the AppDomain.UnhandledException event. This event will be raised immediately upon the exception occurring.

It is important to note that after your handler has finished executing, the process will be shut down. There is no way to prevent this happening. However, you can soften the blow by calling Environment.Exit(0) as the final step in your handler. This will prevent the OS from generating the crash dump and generally providing a poor user experience.

For example:

AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(
                                            CurrentDomain_UnhandledException);
 
static void CurrentDomain_UnhandledException(object sender,
   UnhandledExceptionEventArgs e)
{
 Exception ex = (Exception)e.ExceptionObject;
 Console.WriteLine("An unhandled exception occurred: {0}", ex);
 Environment.Exit(0);
}
Unhandled exceptions in Tasks

Unlike a Thread, an unhandled exception within a Task does not result in the process being shut down. Instead, the Exception object is stored within the Task object and can be accessed by other parts of the code. It will only be actually thrown if code waits (or ‘awaits’) on the task.

If you use async/await on your tasks (or call Task.Wait), the exceptions will be thrown as normal and can be handled in a catch block, or by the AppDomain.UnhandledException event.

However, if your code creates a Task and runs it, but never directly accesses it again, unhandled exceptions will be lost.

The Task equivalent of the AppDomain.UnhandledException event is TaskScheduler.UnobservedTaskException. An ‘unobserved’ exception is one that is never directly accessed from within the Task object in which the exception occurred. Note that this event will not be raised immediately upon the exception occurring–instead, it will be raised when the Task object is garbage collected. Therefore, it could happen long after the actual exception. At the latest, it will be called when the program is shut down.

It is worth adding a handler to this event and logging the exception details. This will ensure that any unhandled Task exceptions are eventually logged.

TaskScheduler.UnobservedTaskException +=
(object sender, UnobservedTaskExceptionEventArgs e) =>
{
 e.SetObserved();
 ((AggregateException)e.Exception).Handle(ex =>
 {
  Console.WriteLine("Exception: {0}", ex);
  return true;
 });
};

If you want to deal with unhandled Task exceptions immediately, then you can add a special ContinueWith on your Task that will only be called if the task fails. For example:

Task.Run(() =>
            {
                List<int> x = null;
                x.Add(3); // will throw
            })
    .ContinueWith(
      (t) => { Console.WriteLine(t.Exception.InnerException.Message); },
      TaskContinuationOptions.OnlyOnFaulted);

If you have lots of tasks where you want to log unhandled exceptions and/or shutdown afterwards, you could create an extension method to use in place of ContinueWith:

public static Task ShutdownOnFault(this Task t)
    {
        t.ContinueWith(x => {
          Console.WriteLine(t.Exception.ToString());
          SaveData();
          Environment.Exit(0);
        }, TaskContinuationOptions.OnlyOnFaulted);
        return t;
    }
 
...
 
Task.Run(() => {...})
    .ShutdownOnFault();
Common exception types to handle

The following are common exception types that should nearly always be handled in a catch block:

SocketException – thrown by System.Net.Socket if any problem occurs during socket communication, including disconnects, etc.

WebException – thrown by System.Net.WebClient if any problem occurs while downloading/uploading HTTP data.

SqlException – thrown by System.Data.SqlClient classes and autogenerated database methods for any SQL Server related problem.

OperationCanceledException – thrown by various .NET library methods when a task or other operation is cancelled.

IOException – thrown by System.IO classes when a local or network I/O problem occurs. Derived classes include DirectoryNotFoundException, FileNotFoundException, etc.

CommunicationException – thrown by System.ServiceModel (WCF) classes and autogenerated WCF client code.

XmlException – thrown by System.Xml classes if an XML parsing error occurs.

UnauthorizedAccessException – thrown by various I/O classes when a file or other resource cannot be accessed due to lack of permission.

Recommendations
  • Only use catch (Exception) in very specific situations, such as when calling third party code that you want to execute in isolation (e.g. a plugin)
    • You could also place one in each Thread callback (but not in a Task) if it’s acceptable for some threads to shut down whilst others continue
  • Only catch out of memory, casting, null reference, or similar exceptions in extreme circumstances, such as when you are calling third party code with a known bug that you know can be safely ignored
  • Check the .NET documentation (or just hover over a method name in Visual Studio) to see what exceptions a method could throw
    • Decide if you think any of those exceptions could be thrown in your code and can be recovered from – for example if you have already checked a string and you know the characters are all digits, you wouldn’t expect to get a FormatException from int.Parse(). However, if you are accepting freeform user input, then you might well get that exception and should handle it.
    • For each exception type that your program could potentially recover from, add a catch block.
  • Add an event handler for AppDomain.UnhandledException. In the handler, you can log the error and try to shut down gracefully. Call Environment.Exit(0) so that you don’t trigger a crash dump etc.
  • Add an event handler for TaskScheduler.UnobserveredTaskException. In the handler, you can log the error (but don’t have to shut down unless you want to).
  • If required, add a ContinueWith() to each Task to immediately deal with unhandled exceptions
  • When writing library code, think about what exceptions each of your methods could throw and document them in the XML comments. Consider if you could catch some exceptions and wrap them in your own custom exception class. For example, if you have a method that is reading from a web server and writing to a file, then you could catch any WebException or IOException and just throw a ‘MyCustomException’ in its place This would make it easier for calling code to handle errors.

 

Share the Post:

Related Posts