:::: MENU ::::

Monday, May 12, 2008

Exceptions provide a consistent mechanism for identifying and responding to error conditions. Effective exception handling will make code more robust and easier to debug. Exceptions are a tremendous debugging aid because they help answer:

  • What went wrong
  • Where did it go wrong
  • Why did it go wrong

What is answered by the type of exception thrown, where is answered by the exception stack trace, and why is answered by the exception message. If your exception is not answering all three questions, chances are they aren't being used effectively.

Following are some suggestions on Do's and Don'ts for exception handling:

  • Don't throw an exception when a simple if statement can be used to check for errors. Remember an exception is something that is out of the norm. For example, a simple if statement to check whether a connection is closed is much better than throwing an exception for the same.

Do

if(conn.State != ConnectionState.Closed)
    conn.Close(); 

Don’t 

try
{
    conn.Close();
}
catch(InvalidOperationException ex)
{
    //do something with the error or ignore it
} 
  • Do use try/finally blocks around code that can potentially generate an exception and centralize your catch statements in one location. In this way, the try statement generates the exception, the finally statement closes or deallocates resources, and the catch statement handles the exception from a central location. This also has the additional benefit of making your code more readable. Consider a simple example where you want to create a temporary file and delete it afterwards even if an error occurs
string Foo1(string fileName)
{
    string fileContents;
    using(StreamReader sr = new StreamReader(fileName))
    {
        fileContents =sr.ReadToEnd();
    }
    File.Delete(fileName);
    return fileContents;
}
 

This code deletes the file just before the return statement; but what if an exception occurs in the lines above? it leaves the temporary file on the disk. Some people might try to solve it coding as this:

string Foo1(string fileName)
{
    try
    {
        string fileContents;
        using (StreamReader sr = new StreamReader(fileName))
        {
            fileContents = sr.ReadToEnd();
        }
        File.Delete(fileName);
        return fileContents;
    }
    catch (Exception)
    {
        File.Delete(fileName);
        throw;
    }
}

The above code does the job but its a bit more complex and the File.Delete is duplicated in the try and catch blocks. Now, see how the try/finally solution is more cleaner and robust:

string Foo1(string fileName)
{
    try
    {
        using (StreamReader sr = new StreamReader(fileName))
        {
            return sr.ReadToEnd();
        }
    }
    finally
    {
        File.Delete(fileName);
    }
}

Notice that we haven't used the fileContents local variable now because we can return the contents of the ReadToEnd() method and the cleanup code executes after the return point. This is one of the advantages of having code that can run after the function returns: you can clean resources that may be needed for the return statement.

 

  • Don’t use finally blocks if all you need to do is call Dispose() on your object. Instead use "using" for objects which support the IDisposable interface. "using" will guarantee that the Dispose() method is called even if an exception occurs within the code block.
  • Do postfix your exception class names with the word "Exception". For example:
public class FileNotFoundException : IOException {
} 
  • When creating user-defined exceptions, you must ensure that the metadata for the exceptions is available to code executing remotely, including when exceptions occur across application domains. For example, suppose Application Domain A creates Application Domain B, which executes code that throws an exception. For Application Domain A to properly catch and handle the exception, it must be able to find the assembly containing the exception thrown by Application Domain B. If Application Domain B throws an exception that is contained in an assembly under its application base, but not under Application Domain A's application base, Application Domain A will not be able to find the exception and the common language runtime will throw a FileNotFoundException. To avoid this situation, you can deploy the assembly containing the exception information in two ways:
    • Put the assembly into a common application base shared by both application domains
      - or -
    • If the domains do not share a common application base, sign the assembly containing the exception information with a strong name and deploy the assembly into the global assembly cache.

In situations where you are using remoting, you must ensure that the metadata for any user-defined exceptions is available at the server (callee) and to the client (the proxy object or caller).

  • Do use at least the three common constructors when creating your own exception classes. Failure to do so means your exception class will not be able to follow some of these Do's and Don’t's.
public class FileNotFoundException : IOException
{
    public FileNotFoundException()
    {
    }
 
    public FileNotFoundException(string message) : base(message)
    {
    }
 
    public FileNotFoundException(string message, Exception inner) : base(message, inner)
    {
    }
}
 
  • Do use the predefined exceptions types for common cases and define your own custom exception types only for programmatic scenarios. For example, to enable a programmer to take a different action in code based on the exception class.
  • Do remember that exceptions are classes which can have properties for extra information. Include extra information in an exception (in addition to the description string) when there is a programmatic scenario where the additional information is useful. Don’t try and cram all information in the Message property.
  • Design classes so that an exception is never thrown in normal use. For example, a FileStream class exposes another way of determining whether the end of the file has been reached. This avoids the exception that is thrown if you read past the end of the file. The following example shows how to read to the end of the file.
·         class FileRead
·         {
·             public void Open(FileStream fileToRead) 
·             {
·                 if (fileToRead == null)
·                 {
·                     throw new System.ArgumentNullException();
·                 }
·                 int b;
·                 fileToRead.Seek(0, SeekOrigin.Begin);
·                 for (int i = 0; i < fileToRead.Length; i++)
·                 {
·                     b = fileToRead.ReadByte();
·                     Console.Write(b.ToString());
·                 }
·             }
} 
  • Do throw an InvalidOperationException if a property set or method call is not appropriate given the object's current state. For example, if a OpenSocket() method was unable to allocate the resource and returned null a subsequent Listen() method should throw a InvalidOperationException.
  • Do throw exceptions instead of returning special error codes or HRESULT.
    • Exceptions makes the common case faster, because when you return special values from methods, each method return needs to be checked and this consumes at least one processor register more, leading to slower code.
    • Special values can, and will be ignored. I've seen dozens of newsgroup posts asking for if there was some way to check if the method succeeded or not and turns out the method did return an enum of possible return codes.
    • Even if the return code was checked by the programmer its not much use as it does not tell me what, where and why went wrong. Its just a simple code which I may be able to lookup in the documentation but then again it could be a generic error code which simply tells me that the method failed and not why it failed. For all I know it could have failed because of some IOException or some ArgumentException.
    • There may not be a suitable value to return that can be used as an indicator for an error condition. I've seen countless methods return -1 to indicate an error condition but its not always suitable. What value would you return from the following function to represent a division by zero?
  • Do return special error code (null) for extremely common situations instead of exception. Wait a minute, wasn’t the point above about not to return error codes? What gives? It turns out most methods which return some resource in the .NET framework follow this rule. For example, Open returns null if the file is not found, but throws an error if the file cannot be opened (file is locked by some other process). But why this alternate rule for null?
    • null would be out of the norm and hence less code will be executed for the common case.
    • null cannot be ignored. Trying to ignore it will most likely throw an NullReferenceException.
  • Do clean up intermediate results and resources when throwing an exception. Callers should be able to assume that there are no side effects when an exception is thrown from a method. For example, if an exception is thrown from Foo1() then it should properly dispose of the objects before leaving the method.
public void Foo1()
{
    using(SqlConnection oConn = new SqlConnection())
    {
        try
        {
            SqlCommand oCmd = oConn.CreateCommand();
            //call some proc
            oConn.Open();
        }
        catch(SqlException ex)
        {
            //handle exception
            throw;
        }
        finally
        {
            oConn.Close();
        }
    }
}
 
  • Don’t throw Exception(). It is too broad class and catching it could mean swallowing other exceptions. Instead create your own custom exceptions and derive from Exception class.
[Serializable]
class MyCustomException : Exception
{
}
 
  • Do mark your custom exceptions with [Serializable]. Your method might very well be called from remoting components or web services and if your exception is not serializable you will end up with a completely different exception hiding your original exception, making any bug extremely hard to debug and fix.
  • Don’t swallow an exception by putting an empty catch block. Never do this. If you don’t need to do anything when an exception is thrown then just don’t catch it and let other code try and handle it. Many times I've seen novice programmers will try and put try...catch on all events in an aspx page and catch(Exception ex) without doing anything with that exception. This effectively hides all errors from the user but what if this event was a button click for submitting an online shopping cart? The try…catch will hide the fact that the request failed but will not show it to the user, this will ultimately result in a customer support call by a frustrated user.

The first class MyClass is on an assembly, and the second class (MyCCValidator) is on another assembly. On the development machine the code ran right, but on the QA machines, the code always returned "Invalid number", even if the entered number was valid.

public class MyClass
{
    public static string ValidateNumber(string userInput)
    {
        try
        {
            bool val = MyCCValidator.Validate(userInput);
            return "Valid number";
        }
        catch (Exception)
        {
            return "Invalid number";
        }
    }
}
 
public class MyCCValidator
{
    public static bool Validate(string userInput)
    {
        return true;
    }
}

The problem was on our setup, which didn't include the second assembly (ValidatorsLibrary). Now, we had a FileNotFoundException when the Validate was called, and the code assumed that it was because the number was invalid.

  • Don’t catch Exception. Always use the most specific exception for the code you are writing. Remember good code is not code that doesn’t throw exceptions. Good code throws exceptions as needed and handles only the exceptions it knows how to handle. The more specific the exception, the better your code answers what went wrong. For example, your code may respond to a FileNotFoundException by asking the user for a different file name.
File prefsFile = new File(prefsFilename);
try
{
    readPreferences(prefsFile);
}
catch (FileNotFoundException e)
{
    // alert the user that the specified file does not exist
}
catch (EOFException e)
{
    // alert the user that the end of the file was reached
}
catch (ObjectStreamException e)
{
    // alert the user that the file is corrupted
}
catch (IOException e)
{
    // alert the user that some other I/O error occurred
}

Assuming IOException is the base class for FileNotFoundException, EOFException and ObjectStreamException. Should an IOException other than those specified by the first three catch blocks be thrown, the last catch block will handle it by presenting the user with a somewhat more generic error message. This way, the program can provide specific information when possible, but still handle the general case should an unanticipated file-related exception slip by.

 

  • Don't use throw ex. Out of every property in the Exception class, the stack trace is one of the most useful information that an exception carries. If you need to put some exception handling in the catch block and re-throw the exception so it can be logged / handled in the UI block with an appropriate message:

Don't

try
{
    //code that throws an exception
}
catch(Exception ex)
{
    //handle exception
    throw ex;
} 

Do

try
{
    //code that throws an exception
}
catch(Exception ex)
{
    //handle exception
    throw;
} 

When you examine the stack trace of the first code sample the point of the exception will be the line "throw ex;" hiding the real error location. "throw;" on the other hand will simply rethrow the exception and will keep your stack trace intact.

  • Don’t log Exception.Message. Always log Exception.ToString() instead of Exception.Message. If you only log Exception.Message, you'll only have something like "Object reference not set to an instance of an object" in your exception log, all this tells me is that some where an exception occurred and some object was set to null. It answers the what but not the where and why. Exception.ToString() will give you a stack trace, the inner exception and the message.
  • Do use Debug.Assert to validate arguments to your methods during development only (debug build). Assert will throw an exception if the expression evaluates to false. This can be very useful in preventing null (or any invalid argument) being passed as an argument to your method. But don’t forget that Debug.Assert is removed from release code. For release builds you should throw ArgumentException or a class derived from ArgumentException if invalid parameters are passed.

The above list is just some suggestions from my experiences. Consider them as a starting point for your own exception handling rules. Also, there are lots of good frameworks and libraries which deal with exception handling and logging. One such is the Microsoft Enterprise Library which contains Exception Handling and Logging Application Blocks. These libraries can be great tools in the hands of a programmer who follows strict design guidelines for exception handling and useless if you don’t follow such rules.

In the end, the hardest part of debugging usually is not fixing the bug, but instead finding where in code the bug hides. By following the above list you can use exceptions to help you track down and eradicate bugs and make your programs more robust and user-friendly.

More

Categories: ,