DekGenius.com
[ Team LiB ] Previous Section Next Section

Chapter 5. Exception Handling

This chapter contains recipes covering the exception handling mechanism, including the try, catch, and finally blocks. Along with these recipes are others covering the mechanisms used to throw exceptions manually from within your code. The final types of recipes include those dealing with the Exception classes, their uses, and subclassing them to create new types of exceptions.

Often the design and implementation of exception handling is performed later in the development cycle. But with the power and complexities of C# exception handling, you need to plan and even implement your exception handling scheme much earlier in the development cycle. Doing so will increase the reliability and robustness of your code while minimizing the impact of adding exception handling after most or all of the application is coded.

Exception handling in C# is very flexible. It allows you to choose a fine- or coarse-grained approach to error handling and any level between. This means that you can add exception handling around any individual line of code (the fine-grained approach), around a method that calls many other methods (the coarse-grained approach), or use a mix of the two. When using a fine-grained approach, you can intercept specific exceptions that might be thrown from just a few lines of code. The following method sets an object's property to a numeric value using fine-grained exception handling:

protected void SetValue(object value)
{
    try
    {
        myObj.Property1 = value;
    }
    catch (Exception e)
    {
        // Handle potential exceptions arising from this call here.
    }
}

Consequentially, this approach can add a lot of extra baggage to your code if used throughout your application. This fine-grained approach to exception handling should be used when you have a single line or just a few lines of code that have a high probability of throwing an exception and you need to handle that exception in a specific manner. For example, using the previous SetValue method, we may have to inform the user that an exception occurred and provide a chance to try the action again. If a method exists on myObj that needs to be called whenever an exception is thrown by one of its methods, we should make sure that this method is called at the appropriate time.

Coarse-grained exception handling is quite the opposite; it uses fewer try-catch or try-catch-finally blocks. One example would be to place a try-catch block around all of the code in every public method in an application or component. Doing this allows exceptions to be handled at the highest level in your code. If an exception is thrown at any location in your code, it will be bubbled up the call stack until a catch block is found that can handle it. If try-catch blocks are placed on all public methods (including the Main method), then all exceptions will be bubbled up to these methods and handled. This allows for much less exception handling code to be written, but your ability to handle specific exceptions that may occur in particular areas of your code is diminished. You must determine how best to add exception handling code to your application. This means applying the right balance of fine- and coarse-grained exception handling in your application.

C# allows catch blocks to be written without any parameters. An example of this is shown here:

public void CallCOMMethod( )
{
    try
    {
        // Call a method on a COM object.
        myCOMObj.Method1( );
    }
    catch
    {
        // Handle potential exceptions arising from this call here.
    }
}

This catch block has no parameters. This is a holdover from C++, where exception objects did not have to be derived from the Exception class. Writing a catch clause in this manner in C++ allows any type of object thrown as an exception to be caught. However, in C#, only objects derived from the Exception base class may be thrown as an exception. Using the catch block with no parameters allows all exceptions to be caught, but you lose the ability to view the exception and its information. A catch block written in this manner:

catch
{
    // NOT Able to write the following line of code.
    //Console.WriteLine(e.ToString);
}

is equivalent to this:

catch (Exception e)
{
    // Able to write the following line of code.
    Console.WriteLine(e.ToString);
}

except that the Exception object can now be accessed.

Avoid writing a catch block without any parameters. Doing so will prevent you from accessing the actual Exception object that was thrown.


When catching exceptions in a catch block, you should determine up front when exceptions need to be rethrown, when exceptions need to be wrapped in an outer exception and thrown, and when exceptions should be handled immediately and not be rethrown.

Wrapping an exception in an outer exception is a good practice when the original exception thrown would not make sense to the caller. When wrapping an exception in an outer exception, you need to determine what exception is most appropriate to wrap the caught exception. As a rule of thumb, the wrapping exception should always aid in tracking down the original problem.

Another useful practice to use when catching exceptions is to use specific catch blocks to handle specific exceptions in your code. When using specific catch blocks, consider adding a catch block that handles all other exceptions (Exception) if you need to handle any other unexpected exception or make sure that all other exceptions are handled at some point in your code. Also, remember that base class exceptions—when used in a catch block—catch that type as well as all of its subclasses. The following code uses specific catch blocks to handle different exceptions in the appropriate manner:

public void CallCOMMethod( )
{
    try
    {
        // Call a method on a COM object.
        myCOMObj.Method1( );
    }
    catch (System.Runtime.InteropServices.ExternalException exte)
    {
        // Handle potential COM exceptions arising from this call here.
    }
    catch (InvalidOperationException ae)
    {
        // Handle any potential method calls to the COM object which are 
        // not valid in its current state.
    }
}

In this code, any ExternalException or its derivatives are handled differently than any thrown InvalidOperationException or its derivatives. If any other types of exceptions are thrown from the myCOMObj.Method1, they are not handled here and are bubbled up until a valid catch block is found. If none are found, the exception is considered unhandled and the application will terminate.

At times, cleanup code must be executed regardless of whether an exception is thrown. Any object must be placed in a stable known state when an exception is thrown. In these situations where code must be executed, use a finally block. The following code has been modified (see boldface lines) to use a finally block:

public void CallCOMMethod( )
{
    try
    {
        // Call a method on a COM object.
        myCOMObj.Method1( );
    }
    catch (System.Runtime.InteropServices.ExternalException exte)
    {
        // Handle potential COM exceptions arising from this call here.
    }
    finally
    {
        // Clean up and free any resources here.
        //   For example, there could be a method on myCOMObj to allow us to clean
        //   up after using the Method1 method.
    }
}

The finally block will always execute, no matter what happens in the try and catch blocks. The finally block even executes if a return, break, or continue statement is executed in the try or catch blocks, or if a goto is used to jump out of the exception handler. This setup allows for a reliable method of cleaning up after the try (and possibly catch) block code executes.

When determining how to structure your exception handling in your application or component, consider doing the following:

  • Use a single try-catch or try-catch-finally exception handler at locations higher up in your code. These exception handlers could be considered coarse-grained.

  • Code farther down the call stack should contain try-finally exception handlers. These exception handlers can be considered fine-grained.

The fine-grained try-finally exception handlers allow for better control over cleaning up after an exception occurs. The exception is then bubbled up to the coarser- grained try-catch or try-catch-finally exception handler. This technique allows for a more centralized scheme of exception handling, and minimizes the code that you have to write to handle exceptions.

To improve performance, you should programmatically handle the case when an exception could be thrown versus catching the exception after it is thrown. For example, if a method has a good chance of returning a null value, you could test the returned value for null before that value is used, as opposed to using a try-catch block and allowing the NullReferenceException to be thrown. Remember that throwing an exception has a negative impact on performance and exception-handling code has no noticeable impact on performance, as long as an exception is not thrown. To illustrate this, we take a method that uses exception handling code to handle the NullReferenceException:

public void SomeMethod( )
{
    try
    {
        Stream s = GetAnyAvailableStream( );
        Console.WriteLine("This stream has a length of " + s.Length);
    }
    catch (Exception e)
    {
        // Handle a null stream here.
    }
}

and convert this method to use an if-else conditional to handle the NullReferenceException as:

public void SomeMethod( )
{
    Stream s = GetAnyAvailableStream( );
    
    if (s != null)
    {
        Console.WriteLine("This stream has a length of " + s.Length);
    }
    else
    {
        // Handle a null stream here.
    }
}

Additionally, you should also make sure that this stream was closed, by using the finally block in the following manner:

public void SomeMethod( )
{
    Stream s = null;
    try
    {
        s = GetAnyAvailableStream( );
    
        if (s != null)
        {
            Console.WriteLine("This stream has a length of " + s.Length);
        }
        else
        {
            // Handle a null stream here.
        }
    }
    finally
    {
        s.Close( );
    }
}

The finally block contains the method call that will close the stream, ensuring that there is no data loss.

Consider throwing exceptions instead of returning HRESULTs or some other type of error code. With well-placed exception handling code, you should not have to rely on methods that return error codes such as an HRESULT or a Boolean true/false to correctly handle errors, which makes for much cleaner code. Another benefit is that you do not have to look up any HRESULT values or any other type of error code to understand the code. However, the biggest advantage is that when an exceptional situation arises, you cannot just ignore it as you can with error codes.

This technique is especially useful when writing a managed C# component that is called by one or more COM objects. Throwing an exception is much cleaner and easier to read than returning an HRESULT. The managed wrapper that the runtime creates for your managed object will make a clean and consistent conversion between the exception type and its corresponding HRESULT value.

Throw specific exceptions, not general ones. For example, throw an ArgumentNullException instead of ArgumentException, which is the base class of ArgumentNullException. Throwing an ArgumentException just tells you that there was a problem with a parameter value to a method. Throwing an ArgumentNullException tells you more specifically what the problem with the parameter really is. Another potential problem is that a more general thrown exception may not be caught if you are looking for a more specific type derived from the thrown exception.

There are several types of exceptions built-in to the FCL that you will find very useful to throw in your own code. Many of these exceptions are listed here with a definition of where and when they should be thrown:

  • Throw an InvalidOperationException in a property, indexer, or method when one is called while the object is in an inappropriate state. This state could be caused by calling an indexer on an object that has not yet been initialized or calling methods out of sequence.

  • Throw ArgumentException if invalid parameters are passed into a method, property, or indexer. The ArgumentNullException, ArgumentOutOfRangeException, and InvalidEnumArgumentException are three subclasses of the ArgumentException class. It is more appropriate to throw one of these subclassed exceptions since they are more indicative of the root cause of the problem. The ArgumentNullException indicates that a parameter was passed in as null and that this parameter cannot be null under any circumstance. The ArgumentOutOfRangeException indicates that an argument was passed in that was outside of a valid acceptable range. This exception is used mainly with numeric values. The InvalidEnumArgumentException indicates that an enumeration value was passed in that does not exist in that enumeration type.

  • Throw a FormatException when an invalid formatting parameter is passed in as a parameter to a method. This technique is mainly used when overriding/overloading methods such as ToString that can accept formatting strings.

  • Throw ObjectDisposedException when a property, indexer, or method is called on an object that has already been disposed. This exception should be thrown inside of the called property, indexer, or method.

Many exceptions that derive from the SystemException class, such as NullReferenceException, ExecutionEngineException, StackOverflowException, OutOfMemoryException, and IndexOutOfRangeException are thrown only by the CLR and should not be explicitly thrown with the throw keyword in your code.

    [ Team LiB ] Previous Section Next Section