1.3. Exception Handling Best Practices

Now that we've discussed how to handle and throw exceptions in theory, let's examine some principles and design patterns that will best help us do that in practice.

1.3.1. Defensive Programming

If you drive a car, you probably have automobile insurance. But even though you may be covered by insurance, an accident can be pretty expensive, financially and otherwise. You would probably agree that no matter how much insurance you may have in place, it's smart to be a "defensive driver" and avoid getting into an accident in the first place.

Exceptions are like accidents, and exception handling is like car insurance. While it's vitally important to have and though it may get you "back on the road," it's no substitute for good defensive programming. Exceptions are "expensive" in terms of the resources and additional overhead they incur. Because of this, you should anticipate when, where, and how exceptions could be thrown, and do your best to "steer" your code around them.

1.3.1.1. Validate Input

Input validation is probably the most important step you can take to ensure that your application doesn't have to deal with bad input in the first place. As you'll recall, the code in Listings 1 through 5 uses a GetPathToLog method to get the path. Now assume that GetPathToLog relies on the user to enter a path into a textbox. By adding some validation to that textbox, you can eliminate some of the exception cases you have to deal with. Listing 8 shows an example.

Example 8. Input validation
<asp:TextBox ID="txtPath" runat="server" MaxLength="248" />
<asp:RequiredFieldValidator ID="reqval_txtPath" runat="server"
   ControlToValidate="txtPath" ErrorMessage="Path is required." />
<asp:RegularExpressionValidator ID="regex_txtPath" runat="server"
   ControlToValidate="txtPath"
   ErrorMessage="Path is invalid."
   ValidationExpression=
    '^([a-zA-Z]:)(\{1}|((\{1})[^\]([^/:*?<>"|]*(?<![ ])))+)$' />

You can see that by using a RequiredFieldValidator, you ensure that path can never be null or empty. By using a RegularExpressionValidator, you ensure that the path cannot contain invalid characters or be in an invalid format. And by setting the MaxLength property on the textbox, you ensure that the path cannot be too long. Because these validations have eliminated the possibility of some exceptions being thrown, you don't need to worry about handling them.

NOTE

Validate or restrict user input to reduce the likelihood of exceptions being thrown.

1.3.1.2. Check for Error Conditions

An exception can only tell you that an error has already occurred. However, you often have the option of programmatically determining if an error condition could potentially occur before it actually does.

The option you choose — exception handling or programmatically checking for error conditions — depends on whether the condition is truly exceptional (a state that occurs infrequently or unexpectedly) or non-exceptional (a normal and predictable condition). If the condition can be considered exceptional, then using exception handling is more efficient because fewer lines of code are executed under normal conditions. If the condition is non-exceptional, then programmatic checking is more efficient because fewer exceptions will be thrown.

This is best illustrated by an example. Let's say that you have a blog application in which you allow users to rate blog posts from 1 to 5. Let's also say that you want to display the average rating for each post. If you store the total rating a post has received, along with the number of times it has been rated, you can calculate the average rating as shown in Listing 9.

Example 9. AverageRating property
public double AverageRating
{
   get { return (double) TotalRating / NumOfVotes; }
}

This works great — as long as the article has been rated by at least one person. But what if the article has never been rated? If NumOfVotes is 0, the code in Listing 9 will fail with a DivideByZeroException.

Now, let's see what we can do to fix this. Take a look at Listing 10.

Example 10. AverageRating property with exception handling
public double AverageRating
{
   get
   {
      try
      {
         return (double) TotalRating / NumOfVotes;
      }

catch (DivideByZeroException)
      {
         return 0.0;
      }
   }
}

Well, this takes care of the case in which the article has never been rated, and that's what we wanted to do. However, this solution is less than ideal because it allows an exception to be thrown in what really can't be considered an exceptional circumstance. That a post has not been rated is a totally normal and predictable state of affairs. Relying on an exception to handle normal cases like this is like parking your car on the train tracks — it's only a matter of time before the inevitable happens.

Now consider Listing 11.

Example 11. AverageRating property with value checking
public double AverageRating
{
   get
   {
      if (NumOfVotes == 0)
      {
         return 0.0;
      }
      else
      {
         return (double) TotalRating / NumOfVotes;
      }
   }
}

Listing 11 presents a better solution, because it takes unrated posts into account without relying on an exception to do so.

The above example shows how to avoid a DivideByZeroException by testing a divisor's value before performing division arithmetic. There are several other defensive programming strategies you can use to avoid many commonly thrown CLR exceptions.

  • Check for Nulls — Trying to use a null object in code will result in a NullReferenceException. If there is any possibility an object could be null, check for that before calling its methods or accessing its properties.

  • Check Object State — The current state of an object may prevent certain methods from being called on it. For example, attempting to call the Close method on a connection that is already closed will throw an InvalidOperationException. Check the state of an object before performing operations that require a specific state.

  • Check Your Arguments — Many methods and constructors require arguments to be non-null, in a certain format, or within a range of acceptable values. For example, attempting to create a DateTime of February 30, 2009 will result in an ArgumentOutOfRangeException because February doesn't have 30 days. Make sure that the arguments you pass conform to the expectations of the method you are invoking.

  • Be Careful When Working with Arrays — Remember that the elements of an array are of a certain type and that arrays are always of a fixed length. Attempting to store an element of the wrong type in an array throws an ArrayTypeMismatchException. Attempting to access an element with an index less than zero or greater than the size of the array throws an IndexOutOfRangeException. Try to determine if these conditions could occur, and write your code accordingly.

  • Establish Bounds for Iterations and Recursions — While recursive methods (methods that call themselves during execution) are sometimes very handy, you need to guard against the possibility of infinite recursion — lest your execution fall into an endless loop, resulting in a nasty StackOverFlowException. The StackOverFlowException is unique in the .NET Framework because, unlike most other exceptions, it cannot be caught in a try-catch construct. Therefore, this is one of those rare situations in which programmatic handling is your only choice. Make sure that you don't accidentally code an endless loop by failing to define a condition that, when met, will cause the recursion to end.

  • Use Safe Casting — If you attempt to cast an object to a type with which it is not compatible, an InvalidCastException will result. This usually happens when the type of the object you are casting is not known at design time. In these cases, you can use the as or is operators to prevent the exception. The as operator first checks if the cast can be performed successfully, and if so, returns the result of the cast; otherwise, null is returned. The is operator only returns true if the cast would succeed, and false otherwise, without actually casting the object.

  • Use Safe Parsing — In .NET, value types expose Parse and TryParse methods, both of which convert a string representation of a value to an actual value of the type. The difference between the two is that Parse throws a FormatException if the string cannot be converted, while TryParse only returns a Boolean value — true if the conversion succeeds, and false if it does not — while returning the actual parsed value as an out parameter. When performing parsing operations you suspect may fail, you can use TryParse to prevent an exception from being thrown.

Keep in mind that programmatic testing doesn't make sense in all cases. For example, you might think that checking the value of File.Exists before calling File.Open is a good idea, but a race condition exists in which the file may be deleted in the split second that elapses between the two method calls. Therefore, in this case, you should handle the FileNotFoundException, by prompting the user for a different filename or by creating the file.

NOTE

Programmatically check for ordinary cases that might cause an error condition. Allow exceptions to be thrown only in exceptional cases, or when a race condition makes testing impractical.

This is by no means an exhaustive list, but you get the idea. As you code, think carefully about which error conditions may arise at runtime, devise defensive programming strategies to prevent your code from causing exceptions when appropriate, and handle other exceptions as needed.

1.3.2. Choose the Right Exception to Throw

It's most useful to throw an exception that describes an error as precisely as possible. That means that when throwing an exception in response to an invalid argument (including value in a property setter clause), in most cases, you should throw an ArgumentException or a type derived from ArgumentException.

For example, in Listing 6, you threw an ArgumentNullException, rather than a NullReferenceException, and an ArgumentOutOfRangeException, rather than a PathTooLongException. ArgumentException and its derived types have a property called ParamName, which you should set to the name of the argument that caused the exception. If the exception is thrown from the set accessor of a property, you can use the word value for ParamName.

If the method has no parameters, or if the failure would be caused by reasons other than invalid arguments, in most cases you can throw an InvalidOperationException. In Listing 6, you caught an UnauthorizedAccessException and added more detail to it. Since this exception wasn't caused by an invalid argument passed to the method, you threw a new InvalidOperationException that wrapped the original exception.

NOTE

Most of the time, the exceptions you throw from your helper or lower-tier methods should be of type ArgumentException or InvalidOperationException, or an exception derived from either of these types.

Only consider throwing other CLR exceptions if none of the above exceptions meets your needs, or if the exception makes the error ambiguous (e.g., if the same exception could be thrown from the same method for a different reason). Try to throw the most derived exception that fits the situation. Here are a few examples:

  • FormatException — If an argument is not properly formatted. For example, calling DateTime.Parse("xyz123") throws a FormatException, because the argument isn't a valid representation of a date.

  • InvalidEnumArgumentException — If an argument is outside the allowable values for an enumeration

  • NotSupportedException — When an operation is not supported

  • ObjectDisposedException — When an attempt is made to call a method on a disposed object

  • OverflowException — When an arithmetic, casting, or conversion operation would result in an overflow condition in an unchecked context

Keep in mind that composing an accurate, coherent, and well-detailed exception message is often more important than the specific type of exception you throw. The main reason for throwing a specific exception type is to provide the ability to handle them in type-filtered catch blocks. For this reason, you should avoid throwing very general exception types, and never, ever throw the main base types: System.Exception, System.SystemException, or System.ApplicationException.

If you truly cannot find a suitable exception to throw among those listed above — for example, if you need to include very specialized custom properties that consumers of your method can use in programmatic scenarios — you can derive your own custom class from System.Exception. While custom exceptions are used frequently in reusable libraries, it's very rare that a custom exception would be required in a typical ASP.NET application.

1.3.3. Exceptions Should Be Exceptional

While the above section discusses how to throw an exception, keep in mind that throwing an exception is not always the best course of action. If you expect that a given condition could happen naturally as a matter of course, consider returning null (or for methods that return value types, an appropriate default value) instead of throwing an exception. For example, the .NET Membership.GetUser(username) method throws an ArgumentNullException if username is null, but returns null if a user with the name username is not found in the database.

NOTE

Throw exceptions only in exceptional cases.

You can also consider implementing the X-TryX pattern, in which you provide two different methods that implement the same functionality. The first implementation (X) returns the expected result if the operation can be successfully carried out and throws an exception if it can't be. The second implementation (TryX) never throws an exception; instead, it only returns a Boolean value indicating success or failure, while returning the actual result as an out parameter (if the operation fails, a default value parameter is returned). In that way, anyone using your TryX method only needs to test the return value, instead of handling an exception. The .NET value types use this pattern in their Parse and TryParse methods.

1.3.4. Don't Abuse Exceptions

Exception objects have only one purpose — to represent a runtime error, nothing more. Exceptions should never be used for purposes for which they were not intended.

Take a look at Listing 12, which contains a helper method for determining whether an integer is even or odd, and some page code-behind that uses the method.

Example 12. Exception abuse
public static class NumberHelpers
{
   public static ApplicationException EvenOrOdd(int integer)
   {
      if (integer % 2 == 0)
      {
         return new ApplicationException("The integer is even.");
      }
      else

{
         return new ApplicationException("The integer is odd.");
      }
   }
}

...

protected void btnTest_Click(object sender, EventArgs e)
{
   try
   {
      throw NumberHelpers.EvenOrOdd(Convert.ToInt32(txtIntToTest.Text));
   }
   catch (ApplicationException ex)
   {
      litResult.Text = ex.Message;
   }
}

Wow, this is some pretty heavy exception abuse! First, the EvenOrOdd helper method uses an exception as a return type. (While this is acceptable for a helper method that creates and initializes a real exception, that's not the case here.) Second, the page code that uses that method actually relies on the returned exception being thrown; not in response to an error, but in order to just fulfill its normal function.

The scary thing is that this code actually works! Even so, I hope you'll never write code like this.

NOTE

Don't use an exception as a return type or an out parameter for a method, or to direct the normal flow of execution in your program.

1.3.5. Avoid Swallowing Exceptions

Using an empty catch block — in other words, a handler that doesn't either handle or rethrow an exception — is called swallowing the exception. When you swallow an exception, propagation is halted, the exception object is finalized, and application execution proceeds as if nothing happened.

In most cases, exception swallowing is a bad practice, as it conceals the occurrence of an error and could allow serious flaws in your program to go unnoticed. However, there is a school of thought that holds that swallowing an exception may on rare occasions be useful, especially if the error does not adversely affect the main operation of the program. For example, if a logging operation cannot complete because a file is locked or the database is temporarily unavailable, and you do not wish to expose the resulting exception to your users, swallowing it is probably not a bad idea.

NOTE

Avoid swallowing exceptions unless you have a very good reason for doing so.

1.3.6. Don't Try to Handle Everything

I know what you're probably thinking at this point: "Any code could potentially throw an exception. Does that mean I have to enclose every line of my application in try statements?"

The answer is, absolutely not! Of course, it's true that an exception can be thrown at any time. For example, even when calling very safe-looking code, the computer might suddenly run out of memory, which throws an OutOfMemoryException. Would you know how to handle an OutOfMemoryException in code? Me neither! Since there's nothing you can do about those kinds of errors, there's no point in catching them in the first place.

This should point out the futility of catching System.Exception or System.SystemException, or using a catch statement without an argument (sometimes called a general catch block). Though you may see this done in code samples from time to time (heaven knows I've done it in my own blog once or twice), these samples are usually for illustrative purposes only. Remember, a general catch block catches any and every exception that occurs — even catastrophic system failures over which you have no control.

Blindly catching exceptions everywhere, or catching every possible exception, is a very bad practice, indeed. Remember, the MSDN documents all exceptions that can be directly thrown from methods and constructors in the .NET base class library. Use this as a guide in determining what exceptions might be thrown. Then, think about which specific exceptions you should catch, and why. Finally, develop a strategy to handle those specific exceptions, given the context in which it they are thrown.

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

  • Logging and/or notifying system administrators of the exception, and rethrowing it. (Even then, it's generally not useful to routinely log handled exceptions unless you've got a good reason.)

  • Using a finally block to clean up resources that might be abandoned if an exception occurs

If you don't have a reason to catch an exception, or have no idea what you would do with it if you did, then don't. Let it bubble up the call stack, and let your application-level exception handler take care of it. After all, that's what it's there for.

NOTE

Only catch exceptions that you are prepared to handle.

Although it may not seem like it from the discussion so far, well-written code should actually contain relatively few try-catch blocks.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.223.171.168