The first line of defense in an application
is to check for potential error conditions before performing an
operation. For example, a program can explicitly check whether the
divisor is 0 before performing a calculation or whether a file exists
before attempting to open it:
If Divisor <> 0 Then
' Safe to divide some number by Divisor.
End If
If System.IO.File.Exists("myfile.txt") Then
' You can now open the myfile.txt file.
' However, you should still use exception handling because a variety of
' problems can intervene (insufficient rights, hardware failure, etc.).
End If
Even if you perform this basic level of "quality
assurance," your application is still vulnerable. For example, you have
no way to protect against all the possible file access problems that
occur, including hardware failures or network problems that could arise
spontaneously in the middle of an operation. Similarly, you have no way
to validate a user ID and password for a database before attempting to
open a connection—and even if you did, that technique would be subject
to its own set of potential errors. In some cases, it may not be
practical to perform the full range of defensive checks, because they
may impose a noticeable performance drag on your application. For all
these reasons, you need a way to detect and deal with errors when they
occur.
The solution is structured exception handling. To
use structured exception handling, you wrap potentially problematic
code in the special block structure shown here:
Try
' Risky code goes here (opening a file, connecting to a database, and so on).
Catch
' An error has been detected. You can deal with it here.
Finally
' Time to clean up, regardless of whether or not there was an error.
End Try
The Try statement enables error handling. Any
exceptions that occur in the following lines can be "caught"
automatically. The code in the Catch block will be executed when an
error is detected. And either way, whether a bug occurs or not, the
Finally block of the code will be executed last. This allows you to
perform some basic cleanup, such as closing a database connection. The
Finally code is important because it will execute even if an error has
occurred that will prevent the program from continuing. In other words,
if an unrecoverable exception halts your application, you'll still have
the chance to release resources.
The act of catching an exception neutralizes it. If
all you want to do is render a specific error harmless, you don't even
need to add any code in the Catch block of your error handler. Usually,
however, this portion of the code will be used to report the error to
the user or log it for future reference. In a separate component (such
as a business object), this code might handle the exception, perform
some cleanup, and then rethrow it to the calling code, which will be in
the best position to remedy it or alert the user. Or it might actually
create a new exception object with additional information and throw
that.
1. Catching Specific Exceptions
Structured exception handling is particularly
flexible because it allows you to catch specific types of exceptions.
To do so, you add multiple Catch statements, each one identifying the
type of exception (and providing a new variable to catch it in), as
follows:
Try
' Database code goes here.
Catch err As System.Data.SqlClient.SqlException
' Catches common database problems like connection errors.
Catch err As System.NullReferenceException
' Catches problems resulting from an uninitialized object.
End Try
An exception will be caught as long as it's an
instance of the indicated class or if it's derived from that class. In
other words, if you use this statement:
Catch err As Exception
you will catch any exception, because every exception object is derived from the System.Exception base class.
Exception blocks work a little like conditional
code. As soon as a matching exception handler is found, the appropriate
Catch code is invoked. Therefore, you must organize your Catch
statements from most specific to least specific:
Try
' Database code goes here.
Catch err As System.Data.SqlClient.SqlException
' Catches common database problems like connection errors.
Catch err As System.NullReferenceException
' Catches problems resulting from an uninitialized object.
Catch err As System.Exception
' Catches any other errors.
End Try
Ending with a Catch statement for the base Exception
class is often a good idea to make sure no errors slip through.
However, in component-based programming, you should make sure you
intercept only those exceptions you can deal with or recover from.
Otherwise, it's better to let the calling code catch the original error.
When you're using classes from the .NET Framework,
you may not know what exceptions you need to catch. Fortunately, the
Visual Studio Help can fill you in.
The trick is to look up the method or constructor
you're using in the class library reference. One fast way to jump to a
specific method is to use the Help index—just type in the class name,
followed by a period, followed by the method name, as in File.Open .
If there is more than one overloaded version of the method, you'll see
a page that lists them all, and you'll need to click the one that has
the parameters you want.
Once you find the right method, scroll through the
method documentation until you find a section named Exceptions. This
section lists all the possible exceptions that this method can throw.
For example, if you look up the File.Open() method, you'll find that
possible exceptions include DirectoryNotFoundException,
FileNotFoundException, UnauthorizedAccessException, and so on. You
probably won't write a Catch block for each possible exception.
However, you should still know about all of them so you can decide
which exceptions you want to handle separately.
|
2. Nested Exception Handlers
When an exception is thrown, .NET tries to find a
matching Catch statement in the current method. If the code isn't in a
local structured exception block or if none of the Catch statements
matches the exception, .NET will move up the call stack one level at a
time, searching for active exception handlers.
Consider the example shown here, where the Page.Load event handler calls a private DivideNumbers() method:
Protected Sub Page_Load(ByVal sender As Object, _
ByVal e As EventArgs) Handles Me.Load
Try
DivideNumbers(5, 0)
Catch err As DivideByZeroException
' Report error here.
End Try
End Sub
Private Function DivideNumbers(ByVal number As Decimal, _
ByVal divisor As Decimal) As Decimal
Return number/divisor
End Function
In this example, the DivideNumbers() method lacks
any sort of exception handler. However, the DivideNumbers() method call
is made inside a Try block, which means the problem will be caught
further upstream in the calling code. This is a good approach because
the DivideNumbers() routine could be used in a variety of circumstances
(or if it's part of a component, in a variety of different types of
applications). It really has no access to any kind of user interface
and can't directly report an error. Only the calling code is in a
position to determine whether the problem is a serious one or a minor
one, and only the calling code can prompt the user for more information
or report error details in the web page.
NOTE
In this example, great care is taken to use the
Decimal data type rather than the more common Double data type. That's
because contrary to what you might expect, it is
acceptable to divide a Double by 0. The result is the special value
Double.PositiveInfinity (or Double.NegativeInfinity if you divide a
negative number by 0).
You can also overlap exception handlers in such a
way that different exception handlers filter out different types of
problems. Here's one such example:
Protected Sub Page_Load(ByVal sender As Object, _
ByVal e As EventArgs) Handles Me.Load
Try
Dim Average As Integer = GetAverageCost(DateTime.Now)
Catch err As DivideByZeroException
' Report error here.
End Try
End Sub
Private Function GetAverageCost(saleDate As Date) As Integer
Try
' Use Database access code here to retrieve all the sale records
' for this date, and calculate the average.
Catch err As System.Data.SqlClient.SqlException
' Handle a database related problem.
Finally
' Close the database connection.
End Try
End Function
2.1. Dissecting the Code . . .
You should be aware of the following points:
If an SqlException occurs during the database operation, it will be caught in the GetAverageCost() method.
If
a DivideByZeroException occurs (for example, the method receives no
records but still attempts to calculate an average), the exception will
be caught in the calling Page.Load event handler.
If
another problem occurs (such as a null reference exception), no active
exception handler exists to catch it. In this case, .NET will search
through the entire call stack without finding a matching Catch
statement in an active exception handler and will generate a runtime
error, end the program, and return a page with exception information.
3. Exception Handling in Action
You can use a simple program to test exceptions and
see what sort of information is retrieved. This program allows a user
to enter two values and attempts to divide them. It then reports all
the related exception information in the page (see Figure 1).
Obviously, you can easily prevent this exception
from occurring by using extra code-safety checks, or you can elegantly
resolve it using the validation controls. However, this code provides a
good example of how you can deal with the properties of an exception
object. It also gives you a good idea about what sort of information
will be returned.
Here's the page class code for this example:
Public Partial Class ErrorHandlingTest
Inherits System.Web.UI.Page
Protected Sub cmdCompute_Click(ByVal sender As Object, _
ByVal e As EventArgs) Handles cmdCompute.Click
Try
Dim A, B, Result As Decimal
A = Decimal.Parse(txtA.Text)
B = Decimal.Parse(txtB.Text)
Result = A / B
lblResult.Text = Result.ToString()
lblResult.ForeColor = System.Drawing.Color.Black
Catch err As Exception
lblResult.Text = "<b>Message:</b> " & err.Message & "<br /><br />"
lblResult.Text &= "<b>Source:</b> " & err.Source & "<br /><br />"
lblResult.Text &= "<b>Stack Trace:</b> " & err.StackTrace
lblResult.ForeColor = System.Drawing.Color.Red
End Try
End Sub
End Class
Note that as soon as the error occurs, execution is
transferred to an exception handler. The code in the Try block isn't
completed. It's for that reason that the result for the label is set in
the Try block. These lines will be executed only if the division code
runs error-free.
4. Mastering Exceptions
Keep in mind these points when working with structured exception handling:
Break down your code into multiple Try/Catch blocks:
If you put all your code into one exception
handler, you'll have trouble determining where the problem occurred.
You have no way to "resume" the code in a Try block. This means that if
an error occurs at the beginning of a lengthy Try block, you'll skip a
large amount of code. The rule of thumb is to use one exception handler
for one related task (such as opening a file and retrieving
information).
Report all errors:
During debugging, portions of your application's
error handling code may mask easily correctable mistakes in your
application. To prevent this from happening, make sure you report all
errors, and consider leaving out some error handling logic in early
builds.
Don't use exception handlers for every statement:
Simple code statements (assigning a
constant value to a variable, interacting with a control, and so on)
may cause errors during development testing but will not cause any
future problems once perfected. Error handling should be used when
you're accessing an outside resource or dealing with supplied data that
you have no control over (and thus may be invalid).