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