Integrating Exception Handling Into the Development Cycle
The time to start thinking about exception handling is right after you click File > New Project. Exception handling shouldn't be something you "tack-on" to an application after the fact. Here, I discuss a practical approach to incorporating exception handling into the normal code-writing process.
Recently, my friend Jef Claes wrote me via my contact page. In part, his inquiry reads:
In which part of the development cycle, do you have to start thinking about exception handling? How and when do you decide which exceptions to handle specifically, so they won't kill your whole page?
Geesh… I post a few articles and write a book about exception handling, and all of a sudden I’m Mr. Exception Guy. ;)
In all seriousness though, this is a great question. There are lots of resources out there that provide information about exception handling from a theoretical or academic point of view, but very few that present a practical approach to integrating exception handling into your day-to-day development cycle.
Here, I’d like to present my own general approach to exception handling.
Examine the Possibilities
I have written before about the importance of handling only specific exceptions. Of course, you can’t do that unless you know what exceptions could be thrown from your code. That means the first thing you should do is to discover which exceptions are possible.
How you do that depends on the framework or API you’re using. If you’re using the .NET Framework, the MSDN can tell you which exceptions might be thrown from any given method, constructor, property, event, or indexer.
Consider the following simple code, which writes a string of text to a text file. The string of text to write (txtEntry.Text), as well as a portion of the path value (txtPath.Text), is supplied by the user.
private void LogEntry() { string workingDirectory = ConfigurationManager.AppSettings["directoryToLog"]; if (!string.IsNullOrEmpty(workingDirectory)) { string path = workingDirectory + txtPath.Text; using (StreamWriter writer = File.AppendText(path)) { writer.WriteLine(DateTime.Now.ToString()); writer.WriteLine("Entry: {0}", txtEntry.Text); writer.WriteLine("--------------------"); } lblMessage.Text = "The entry " + txtEntry.Text + " was logged successfully."; } else { lblMessage.Text = "The required configuration setting is missing. The entry " + txtEntry.Text + " was not logged."; } }
We can see the first call is to ConfigurationManager.AppSettings. If you place your cursor on the method in the IDE, the Dynamic Help window will show you a link to the method in the MSDN.
Clicking on this link will take you to the relevant MSDN page. The MSDN will show you any exceptions that could possibly be encountered from accessing this property.
Doing the same with the File.AppendText method yields the following result:
Finally, the WriteLine method shows the following:
As you can see, the Visual Studio IDE and accompanying documentation makes this pretty easy. Keep in mind that when you’re calling a third party API, your experience will definitely vary. A well-written API should include XML documentation that exposes via Intellisense any exceptions that could be thrown; and if you’re lucky, compiled documentation created by a tool such as Sandcastle. If not, you have to rely on whatever the API provides, or perhaps use Reflector to disassemble the binary.
Program Defensively
Now that we have a list of all the possible exceptions that can be thrown from our code, the next step is to determine which exceptions we can prevent, and which we can safely ignore.
In this case, we are supplying part of the path in the configuration file, and this branch of code only runs if that value is not null or empty. Therefore, we can be 100% certain that the argument we pass to File.AppendText will never be null. This lets us safely ignore ArgumentNullException from the File.AppendText method.
Next, we can employ defensive programming techniques to prevent most of the remaining exceptions that can be thrown from File.AppendText. By restricting and validating the user input, you can assure that the path supplied by the user can never be too long, contain invalid characters, or be in an invalid format. This relieves us from having to handle ArgumentException, PathTooLongException, or NotSupportedException. That leaves us two possible exceptions we might have to deal with.
Now, let’s consider the two exceptions that can be thrown from the TextWriter.WriteLine method. The first, ObjectDisposedException, can’t happen in this code because all of the WriteLine calls are made prior to closing the writer; so we’ll ignore it. The second, IOException, would normally be thrown in response to a disk or path error. Since we can’t prevent this from happening in code, this exception is a good candidate for handling.
Determine Which Exceptions To Handle
So far, we’ve taken two steps. First, we identified all possible exceptions. Second, we determined which exceptions can be either effectively prevented or safely ignored.
The remaining exceptions we have to worry about are:
- ConfigurationErrorsException (from ConfigurationManager.AppSettings)
- UnauthorizedAccessException (from File.AppendText)
- DirectoryNotFoundException (from File.AppendText)
- IOException (from writer.WriteLine)
Now, we must decide which of these we can and should handle, and why.
There are legitimate reasons for handling exceptions. Among these are:
- Notifying the user, and if appropriate, allowing them to retry the operation
- Wrapping the exception in a new exception, appending additional relevant detail
- Wrapping a specific exception in a more general exception to prevent revealing sensitive details, such as exceptions thrown from a Web service
- Using a finally block to clean up resources that might be abandoned if an exception occurs
In this simple example, notifying the user is probably sufficient.
private void LogEntry() { string workingDirectory = null; try { workingDirectory = ConfigurationManager.AppSettings["directoryToLog"]; } catch (ConfigurationErrorsException) { lblMessage.Text = "There was a problem accessing the configuration settings. The entry " + txtEntry.Text + " was not logged."; return; } if (!string.IsNullOrEmpty(workingDirectory)) { string path = string.Empty; try { path = workingDirectory + txtPath.Text; using (StreamWriter writer = File.AppendText(path)) { writer.WriteLine(DateTime.Now.ToString()); writer.WriteLine("Entry: {0}", txtEntry.Text); writer.WriteLine("--------------------"); } lblMessage.Text = "The entry " + txtEntry.Text + " was logged successfully."; } catch (UnauthorizedAccessException) { lblMessage.Text = "You do not have the required permissions to access the path " + path + " . The entry " + txtEntry.Text + " was not logged."; } catch (DirectoryNotFoundException) { lblMessage.Text = "Part of the path " + path + " was not found. The entry " + txtEntry.Text + " was not logged."; } catch (IOException) { lblMessage.Text = "There was a problem accessing the path " + path + " . The entry " + txtEntry.Text + " was not logged."; } } else { lblMessage.Text = "The required configuration setting is missing. The entry " + txtEntry.Text + " was not logged."; } }
Note that we’re tailoring the message sent back to the UI to be as informative as possible, without using the original Message property from the handled exception. While the Message property is quite useful for logging and debugging purposes, I generally find it’s not a good idea to pass a framework-generated exception message to your end users. For one, they often tend to be rather cryptic to non-developers. More importantly, it’s possible they could contain sensitive information. While the .NET Framework itself is pretty good at not revealing sensitive information in Message, you’ll find that many third-party APIs are not nearly as conscientious in this regard.
Also note that the order in which you handle exceptions can be important. Here we’re handling DirectoryNotFoundException before IOException. This is because DirectoryNotFoundException is a derived type of IOException, and derived types should always be handled first.
Do or Do Not. There is No Try.
If you can’t successfully handle an exception in code, it is best to let the application fail as quickly as possible.
I can hear some of you now: “What? Are you suggesting that we should let our applications fail?”
The answer is yes… that is, in fact, exactly what I am suggesting.
Part of handling exceptions successfully is facing the fact that you cannot successfully handle everything. Remember, an exception being thrown means your application has already crashed. Sometimes it is possible to handle the exception, giving the application a chance to recover or to retry the operation. Quite often though, you will encounter exceptions for which no sensible handling strategy exists, in which case catching the exception is totally pointless. Only catch exceptions which you are prepared to handle; otherwise let them go.
If you catch exceptions which you subsequently do not handle – usually through the use of general catch blocks – you merely postpone the inevitable. Allowing an application to continue in the face of an unhandled exception is not only wasteful, it’s dangerous. Values might be corrupted. Resources might be left in an unstable or unknown state. This is exactly why exceptions are thrown in the first place – to prevent a program from continuing under these conditions. If you can somehow correct or recover from the condition, then fine. If you cannot, then failing quickly is the best option.
Fail Gracefully
The fact is that in a production application, you never truly let any exception remain “unhandled” in a technical sense – the real question is not whether to handle exceptions, but where to handle them.
What I’m saying here is that you need to make absolutely sure you have a global exception handling mechanism in place. In a Windows application, that usually means handling the AppDomain.CurrentDomain.UnhandledException event. In an ASP.NET application, it means handling the HttpApplication.Error event, either manually or via the built-in ASP.NET default exception handler. This “last-chance” global handler lets you provide a friendly user experience when unrecoverable errors occur, as well as a central location from which to log exceptions that weren’t handled by your code.
Of course, once an exception reaches your global exception handler, it means execution has stopped and your application has failed. That happens – and as we’ve learned, sometimes there’s nothing you can do about it. But there’s no reason your application can’t fail gracefully.
Summary
In this post, I’ve outlined a series of steps you can use to sensibly and effectively integrate exception handling into the development cycle. These can be summarized as follows:
- Use the available documentation to discover which exceptions are possible.
- Determine which exceptions you can safely ignore, based on current state.
- Where possible, use defensive programming techniques to prevent exceptions from being thrown.
- Determine which of the remaining exceptions you should handle, and develop a strategy for doing so.
- Allow any exceptions you cannot completely handle in code to propagate to your default exception handler.
This, in a nutshell, pretty much sums up how I approach exception handling as I write code. Hopefully, you’ll find this useful in your day-to-day routine as well.
Questions? Constructive criticism? Suggestions for improvement? Please let me know what you think in the comments below.
Subscribe to this blog for more cool content like this!
You've been kicked (a good thing) - Trackback from DotNetKicks.com
Pingback from Twitter Trackbacks for Integrating Exception Handling Into the Development Cycle : LeeDumond.com [leedumond.com] on Topsy.com
DotNetBurner - burning hot .net content
Thank you for submitting this cool story - Trackback from DotNetShoutout
You are voted (great) - Trackback from WebDevVote.com
Pingback from Dew Drop – October 15, 2009 | Alvin Ashcraft's Morning Dew
Pingback from Helltime for October 16 « I Built His Cage
Been going blind looking for the Dynamic Help window in the new VS 2010 Beta 2? Me too… until I learned this bit of bad news from a inside source.