My musings about .NET and what not

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

magnifying_glass 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. 

screenshot1

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.

screenshot2

Doing the same with the File.AppendText method yields the following result:

screenshot3

Finally, the WriteLine method shows the following:

screenshot4

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

17_AggressiveDriving 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

Dylan_wrestling_belt1 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.

YODA 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

kerri-strug-8_6 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:

  1. Use the available documentation to discover which exceptions are possible.
  2. Determine which exceptions you can safely ignore, based on current state.
  3. Where possible, use defensive programming techniques to prevent exceptions from being thrown.
  4. Determine which of the remaining exceptions you should handle, and develop a strategy for doing so.
  5. 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!

kick it on DotNetKicks.com

shout it on DotNetShoutOut.com

vote it on WebDevVote.com

Bookmark / Share

    » Similar Posts

    1. Defensive Programming, or Why Exception Handling Is Like Car Insurance
    2. If At First You Don’t Succeed - Retrying Mail Operations in .NET
    3. Friends Don’t Let Friends catch (Exception)

    » Trackbacks & Pingbacks

    1. You've been kicked (a good thing) - Trackback from DotNetKicks.com

      Integrating Exception Handling Into the Development Cycle — October 14, 2009 8:56 AM
    2. Pingback from Twitter Trackbacks for Integrating Exception Handling Into the Development Cycle : LeeDumond.com [leedumond.com] on Topsy.com

      Twitter Trackbacks for Integrating Exception Handling Into the Development Cycle : LeeDumond.com [leedumond.com] on Topsy.com — October 14, 2009 11:39 AM
    3. DotNetBurner - burning hot .net content

      Integrating Exception Handling Into the Development Cycle — October 14, 2009 7:33 PM
    4. Thank you for submitting this cool story - Trackback from DotNetShoutout

      Integrating Exception Handling Into the Development Cycle — October 15, 2009 3:02 AM
    5. You are voted (great) - Trackback from WebDevVote.com

      Integrating Exception Handling Into the Development Cycle — October 15, 2009 6:36 AM
    6. Pingback from Dew Drop – October 15, 2009 | Alvin Ashcraft's Morning Dew

      Dew Drop – October 15, 2009 | Alvin Ashcraft's Morning Dew — October 15, 2009 7:20 AM
    7. Pingback from Helltime for October 16 « I Built His Cage

      Helltime for October 16 « I Built His Cage — October 16, 2009 3:10 PM
    8. 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.

      Dynamic Help Removed From Visual Studio 2010 — November 2, 2009 3:30 PM
    Trackback link for this post:
    http://leedumond.com/trackback.ashx?id=79

    » Comments

    1. Jef Claes avatar

      This was exactly what I was looking for. Great article!

      Jef Claes — October 14, 2009 12:03 PM
    2. Pablo Marambio avatar

      Hey Lee,

      First of all, great article!

      But then: What about creating your own exceptions? There is a non-ending discussion about whether this is cost/efective and if so, about which class to use as base class when creating your exceptions: Exception, ApplicationException, etc.

      Regarding the latter, i have heard that not even Microsoft-authored frameworks use ApplicationException as the base class.

      Pablo Marambio — October 16, 2009 3:59 PM
    3. Lee Dumond avatar

      @Pablo -

      A great question -- though it actually deals with throwing exceptions, not catching them, which is an entirely different topic. This is actually something I plan to blog about in the near future. Be sure to subscribe so you can catch that post when it comes out.

      Short answer: I believe there are very few instances in which creating your own exception is worthwhile. There are many exceptions already built into the CLR, and they cover most situations pretty well. The only circumstance in which I'd consider doing so is when you need to pass information back to the caller which none of the existing exceptions can easily accomodate.

      You are correct about ApplicationException -- Microsoft doesn't recommend using it as a base class (and neither do I), as it derives from System.Exception yet adds absolutely no additional functionality. It remains in the framework mostly for backward compatibility. If you do need to create a custom exception, it's better to derive directly from Exception -- or possiby from one of the other specific exception types, if they contain some functionality you can leverage.

      Lee Dumond — October 16, 2009 4:29 PM
    4. Graeme Foster avatar

      One point I'm not sure about - is it really a good idea to attempt to duplicate the validation logic in order to avoid a path with too many characters, for example? Wouldn't it be better to just catch that exception and avoid the risk of getting the logic wrong, and of the logic changing in the future?

      Graeme Foster — October 19, 2009 2:58 AM

    » Leave a Comment