Chapter 19. Error Handling

Although it is theoretically possible to write a program that perfectly predicts every possible situation that it might encounter, in practice that's very difficult for nontrivial programs. For large applications, it is very difficult to plan for every eventuality. Errors in the program's design and implementation can introduce bugs that give unexpected results. Users and corrupted databases may give the application values that it doesn't know how to manage.

Similarly, changing requirements over time may introduce data that the application was never intended to handle. The Y2K bug is a good example. When engineers wrote accounting, auto registration, financial, inventory, and other systems in the 1960s and 1970s, they never dreamed their programs would still be running in the year 2000. At the time, disk storage and memory were relatively expensive, so they stored years as 2-byte values (for example, 89 meant 1989). When the year 2000 rolled around, the applications couldn't tell whether the value 01 meant the year 1901 or 2001. In one humorous case, an auto registration system started issuing horseless carriage license plates to new cars because it thought cars built in 00 must be antiques.

The Y2K problem wasn't really a bug. It was a case of software used with data that wasn't part of its original design.

This chapter explains different kinds of exceptional conditions that can arise in an application. These range from unplanned data (as in the Y2K problem) to bugs where the code is just plain wrong. With some advance planning, you can build a robust application that can keep running gracefully, even when the unexpected happens.

BUGS VERSUS UNPLANNED CONDITIONS

Several different types of unplanned conditions can derail an otherwise high-quality application. How you should handle these conditions depends on their nature.

For this discussion, a bug is a mistake in the application code. Some bugs become apparent right away and are easy to fix. These usually include simple typographic errors and cases where you misuse an object (for example, by using the wrong control property). Other bugs are subtler and may only be detected long after they occur. For example, a data-entry routine might place invalid characters into a rarely used field in a Customer object. Only later when the program tries to access that field will you discover the problem. This kind of bug is difficult to track down and fix, but there are some proactive steps you can take to make these sorts of bugs easier to find.

An unplanned condition is some predictable condition that you don't want to happen, but that you know could happen despite your best efforts. For example, there are many ways that a simple printing operation can fail. The printer might be unplugged, disconnected from its computer, disconnected from the network, out of toner, out of paper, experiencing a memory fault, clogged by a paper jam, or just plain broken. These are not bugs, because the application software is not at fault. There is some condition outside of the program's control that must be fixed.

Another common unplanned condition occurs when the user enters invalid data. You may want the user to enter a value between 1 and 10 in a text box, but the user might enter 0, 9999, or "lunch" instead.

Catching Bugs

By definition, bugs are unplanned. No reasonable programmer sits down and thinks, "Perhaps I'll put a bug in this variable declaration."

Because bugs are unpredictable, you cannot know ahead of time where a bug will lie. However, you can watch for behavior in the program that indicates that a bug may be present. For example, suppose that you have a subroutine that sorts a purchase order's items by cost. If the routine receives an order with 100,000 items, something is probably wrong. If one of the items is a computer keyboard with a price of $73 trillion, something is probably wrong. If the customer who placed the order doesn't exist, something is probably wrong.

This routine could go ahead and sort the 100,000 items with prices ranging from a few cents to $73 trillion. Later, the program would try to print a 5000-page invoice with no shipping or billing address. Only then would the developers realize that there is a problem.

Rather than trying to work around the problematic data, it would be better if the sorting routine immediately told developers that something is wrong so they can start trying to find the cause of the problem. Bugs are easier to find the sooner they are detected. This bug will be easier to find if the sorting routine notices it, rather than waiting until the application tries to print an invalid invoice. Your routines can protect themselves and the program as a whole by proactively validating inputs and outputs, and reporting anything suspicious to developers.

Some developers object to making routines spend considerable effort validating data that they know is correct. After all, one routine generated this data and passed it to another, so you know that it is correct because the first routine did its job properly. That's only true if every routine that touches the data works perfectly. Because bugs are by definition unexpected, you cannot safely assume that all the routines are perfect and that the data remains uncorrupted.

To prevent validation code from slowing down the application, you can use the Debug object's Assert method to check for strange conditions. When you are debugging the program, these statements throw an error if they detect something suspicious. When you make a release build to send to customers, the Debug.Assert code is removed from the application. That makes the application faster and doesn't inflict cryptic error messages on the user.

You can also use the DEBUG, TRACE, and CONFIG compiler constants to add other input and output validation code.

Example program SortOrders uses the following code to validate a subroutine's inputs. (This program doesn't actually do anything; it just shows how to write input validation code.)

Private Sub SortOrderItems(ByVal the_order As Order)
    ' Validate input.
    Debug.Assert(the_order.Items IsNot Nothing, "No items in order")
    Debug.Assert(the_order.Customer IsNot Nothing, "No customer in order")
    Debug.Assert(the_order.Items.Count < 100, "Too many order items")
    ...

    ' Sort the items.
    ...

    ' Validate output.
#If DEBUG Then
    ' Verify that the items are sorted.
    Dim order_item1 As OrderItem
Dim order_item2 As OrderItem
    order_item1 = DirectCast(the_order.Items(1), OrderItem)
    For i As Integer = 2 To the_order.Items.Count
        order_item2 = DirectCast(the_order.Items(i), OrderItem)
        Debug.Assert(order_item1.Price <= order_item2.Price,
            "Order items not properly sorted")
        order_item1 = order_item2
    Next i
#End If
End Sub
                                                  
AUTOMATED BUG CATCHERS

The subroutine starts by validating its input. It verifies that the Order object that it received has an Items collection and that its Customer property is not Nothing. It also verifies that the order contains fewer than 100 items. If a larger order comes along during testing, developers can increase this number to 200 or whatever value makes sense, but there's no need to start with an unreasonably large default.

Before the subroutine exits, it loops through the sorted items to verify that they are correctly sorted. If any item has cost less than the one before, the program throws an error. Because this test is contained within an #If DEBUG Then statement, this code is removed from release builds.

After you have tested the application long enough, you should have discovered most of these types of errors. When you make the release build, the compiler automatically removes the validation code, making the finished executable smaller and faster.

Catching Unplanned Conditions

Although you don't want an unplanned condition to happen, with some careful thought, you can predict where an unplanned condition might occur. Typically, these situations arise when the program must work with something outside of its own code. For example, when the program needs to access a file, printer, web page, floppy disk, or CD-ROM, that item may be unavailable. Similarly, whenever the program takes input from the user, the user may enter invalid data.

Notice how this differs from the bugs described in the previous section. After sufficient testing, you should have found and fixed most of the bugs. No amount of testing can remove the possibility of unplanned conditions. No matter what code you use, the user may still remove a floppy disk from the drive before the program is ready.

Whenever you know that an unplanned condition might occur, you should write code to protect the program from dangerous conditions. It is generally better to test for these conditions explicitly rather than simply attempting to perform whatever action you were planning and then catching an error if one occurs. Testing for problem conditions generally gives you more complete information about what's wrong. It's also usually faster than catching an error because the structured error handling described shortly comes with considerable overhead.

For example, the following statement sets an integer variable using the value the user entered in a text box:

Dim num_items As Integer = Integer.Parse(txtNumItems.Text)

The user might enter a valid value in the text box. Unfortunately, the user may also enter something that is not a number, a value that is too big to fit in an integer, or a negative number when you are expecting a positive number. The user may even leave the field blank.

Example program ValidateInteger uses the following code to validate integer input:

' Check for blank entry.
Dim num_items_txt As String = txtNumItems.Text
If num_items_txt.Length < 1 Then
    MessageBox.Show("Please enter Num Items")
    txtNumItems.Focus()
    Exit Sub
End If

' See if it's numeric.
If Not IsNumeric(num_items_txt) Then
    MessageBox.Show("Num Items must be a number")
    txtNumItems.Select(0, num_items_txt.Length)
    txtNumItems.Focus()
    Exit Sub
End If

' Assign the value.
Dim num_items As Integer
Try
    num_items = Integer.Parse(txtNumItems.Text)
Catch ex As Exception
    MessageBox.Show("Error in Num Items." & vbCrLf & ex.Message)
    txtNumItems.Select(0, num_items_txt.Length)
    txtNumItems.Focus()
    Exit Sub
End Try

' Check that the value is between 1 and 100.
If num_items < 1 Or num_items > 100 Then
    MessageBox.Show("Num Items must be between 1 and 100")
    txtNumItems.Select(0, num_items_txt.Length)
    txtNumItems.Focus()
    Exit Sub
End If
                                                  
Catching Unplanned Conditions

The code checks that the field is not blank and uses the IsNumeric function to verify that the field contains a vaguely numeric value.

Unfortunately, the IsNumeric function doesn't exactly match the behavior of functions such as Integer.Parse. IsNumeric returns False for values such as &H10, which is a valid hexadecimal value that Integer.Parse can correctly interpret. IsNumeric also returns True for values such as 123456789012345 that lie outside of the values allowed by integers and 1.2, which is numeric but not an integer. Because IsNumeric doesn't exactly match Integer.Parse, the program still needs to use a Try Catch block (bolded in the previous code) to protect itself when it actually tries to convert the string into an integer.

The code finishes by verifying that the value lies within a reasonable bound. If the value passes all of these checks, the code uses the value.

A typical subroutine might need to read and validate many values, and retyping this code would be cumbersome. A better solution is to move it into an IsValidInteger function and then call the function as needed.

You can write similar routines to validate other types of data fields such as phone numbers, e-mail addresses, street addresses, and so on.

Global Exception Handling

Normally, you should try to catch an error as close as possible to the place where it occurs. If an error occurs in a particular subroutine, it will be easiest to fix the bug if you catch it in that subroutine.

However, bugs often arise in unexpected places. Unless you protect every subroutine with error-handling code (a fairly common strategy), a bug may arise in code that you have not protected.

In early versions of Visual Basic, you could not catch the bug, so the application crashed. In the most recent versions of Visual Basic, however, you can define a global error handler to catch any bug that isn't caught by other error-handling code.

To define application-level event handlers, double-click My Project in the Project Explorer. Open the Application tab and click the View Application Events button. This opens a code window for application-level events.

In the left drop-down list, select (MyApplication Events). Then in the right drop-down list, you can select one of several events including NetworkAvailabilityChanged, Shutdown, Startup, StartupNextInstance, and UnhandledException. Select the last of these commands to open the UnhandledException event handler.

In the event handler, you can take whatever action is appropriate for the error. Because you probably didn't anticipate the error, there's usually little chance that the program can correct it properly. However, you can at least log the error and possibly save data before shutting down the application.

The event parameter e has an ExitApplication property that you can set to True or False to tell Visual Basic whether the application should terminate.

Example program GlobalException uses the following code to display a message giving the unhandled exception's error message. It then sets e.ExitApplication to False, so the program keeps running.

Private Sub MyApplication_UnhandledException(
 ByVal sender As Object,
 ByVal e As Microsoft.VisualBasic.ApplicationServices.UnhandledExceptionEventArgs) _
 Handles Me.UnhandledException
    MessageBox.Show("Exception caught globally" & vbCrLf & e.Exception.Message)
    e.ExitApplication = False
End Sub
                                                  
KEEP RUNNING

When you run the application in the IDE, Visual Basic stops execution in the debugger when it reaches the statement that causes the error, so the UnhandledException event never executes. If you run the compiled executable, however, the UnhandledException event fires and the global error-handler runs.

STRUCTURED ERROR HANDLING

Visual Basic .NET introduced structured error handling using the Try block. The syntax is as follows:

Try
  try_statements ...
[Catch ex As exception_type_1
  exception_statements_1 ...
]
[Catch ex As exception_type_2
  exception_statements_2 ...
]
...
[Catch
  final_exception_statements ...
]
[Finally
  finally_statements ...
]
End Try

The program executes the code in the try_statements block. If any of that code throws an exception, the program jumps to the first Catch statement.

If the exception matches exception_type_1, the program executes the code in exception_statements_1. The exception type might match the Catch statement's exception class exactly, or it might be a subclass of the listed class. For example, suppose that the code in the try_statements block performs a calculation that divides by zero. That raises a DivideByZeroException. That class inherits from the ArithmeticException class, which inherits from SystemException, which inherits from Exception. That means the code would stop at the first Catch statement it finds that looks for DivideByZeroException, ArithmeticException, SystemException, or Exception.

If the raised exception does not match the first exception type, the program checks the next Catch statement. The program keeps comparing the exception to Catch statements until it finds one that applies, or it runs out of Catch statements.

If no Catch statement matches the exception, the exception "bubbles up" to the next level in the call stack and Visual Basic moves to the routine that called the current one. If that routine has appropriate error-handling code, it deals with the error. If that routine can't catch the error, the exception bubbles up again until Visual Basic eventually either finds error-handling code that can catch the exception, or it runs off the top of the call stack. If it runs off the call stack, Visual Basic calls the global UnhandledException event handler described in the previous section, if one exists. If there is no UnhandledException event handler, the program crashes.

If you include a Catch statement with no exception type, that block matches any exception. If the raised exception doesn't match any of the previous exception types, the program executes the final_exception_statements block of code. Note that the statement Catch ex As Exception also matches all exceptions, so it's just good as Catch by itself. It also gives you easy access to the exception object's properties and methods.

You can figure out what exception classes to use in Catch statements in several ways. First, you can spend a lot of time digging through the online help. An easier method is to let the program crash and then look at the error message it produces. Figure 19-1 shows the error message a program throws when it tries to convert the non-numeric string "Hello" into an integer with Integer.Parse. From the exception dialog's title, it's easy to see that the program should look for a FormatException.

When a program crashes, the message it generates tells you the type of exception it raised

Figure 19.1. When a program crashes, the message it generates tells you the type of exception it raised

Another way to decide what types of exceptions to catch is to place a final generic Catch ex As Exception statement at the end of the Catch list. Place code inside that Catch block that displays either the exception's type name (use TypeName) or the result of its ToString method. When you encounter new exception types, you can give them their own Catch statements and take more action that's appropriate to that exception type.

After it has finished running the code in try_statements and it has executed any necessary exception code in a Catch block, the program executes the code in finally_statements. You can use the Finally section to execute code whether the code in try_statements succeeds or fails.

You do not need to include any Catch statements in a Try block, but leaving them all out defeats the Try block's purpose. If the try_statements raise an error, the program doesn't have any error code to execute, so it sends the error up the call stack. Eventually, the program finds an active error handler or the error pops off the top of the stack and the program crashes. You may as well not bother with the Try block if you aren't going to use any Catch sections.

A Try block must include at least one Catch or Finally section, although those sections do not need to contain any code. For example, the following Try block calls subroutine DoSomething and uses an empty Catch section to ignore any errors that occur:

Try
    DoSomething()
Catch
End Try

Using an empty Finally section is legal but not terribly useful. The following code doesn't protect the program from any exceptions and doesn't do anything in the Finally block. You may as well just omit the Try block.

Try
    DoSomething()
Finally
End Try

Example program ThrowError, which is available for download on the book's web site, shows how a program can use a Try Catch block to handle errors.

Exception Objects

When a Catch statement catches an exception, its exception variable contains information about the error that raised the exception. Different exception classes may provide different features, but they all provide the basic features defined by the Exception class from which they are all derived. The following table lists the most commonly used Exception class properties and methods.

ITEM

PURPOSE

InnerException

The exception that caused the current exception. For example, suppose that you write a tool library that catches an exception and then throws a new custom exception describing the problem in terms of your library. You should set InnerException to the exception that you caught before you throw the new exception

Message

Returns a brief message that describes the exception.

Source

Returns the name of the application or object that threw the exception.

StackTrace

Returns a string containing a stack trace giving the program's location when the error occurred.

TargetSite

Returns the name of the method that threw the exception.

ToString

Returns a string describing the exception and including the stack trace.

Example program ShowExceptionInfo, which is available for download on the book's web site, displays an exception's Message, StackTrace, and ToString values.

At a minimum, the program should log or display the Message value for any unexpected exceptions so you know what exception occurred. It might also log the StackTrace or the result of ToString so you can see where the exception occurred.

The following text shows the results of the ToString method produced by a DivideByZeroException exception object:

System.DivideByZeroException: Attempted to divide by zero.
    at ShowExceptionInfo.Form1.CheckVacationPay() in C:Documents and
SettingsRodLocal SettingsApplication DataTemporary
ProjectsShowExceptionInfoForm1.vb:line 25
    at ShowExceptionInfo.Form1.CalculateEmployeeSalaries() in C:Documents and
SettingsRodLocal SettingsApplication DataTemporary
ProjectsShowExceptionInfoForm1.vb:line 18
    at ShowExceptionInfo.Form1.btnCalculate_Click(Object sender, EventArgs e) in
C:Documents and SettingsRodLocal SettingsApplication DataTemporary
ProjectsShowExceptionInfoForm1.vb:line 5

The StackTrace and ToString values can help developers find a bug, but they can be intimidating to end users. Even the abbreviated format used by the exception's Message property is usually not very useful to a user. When the user clicks the "Find Outstanding Invoices" button, the message "Attempted to divide by zero" doesn't really tell the user what the problem is or what to do about it.

When a program catches an error, a good strategy is to record the full ToString message in a log file or e-mail it to a developer. Then display a message that restates the error message in terms that the user can understand. For example, the program might say the following: "Unable to total outstanding invoices. A bug report has been sent to the development team." The program should then try to continue as gracefully as possible. It may not be able to finish this calculation, but it should not crash, and it should allow the user to continue working on other tasks if possible.

StackTrace Objects

An exception object's ToString and StackTrace methods return textual representations of the program's stack trace. Your code can also use StackTrace objects to examine the program's execution position without generating an error.

The following code shows how a program can display a simple stack trace in the Immediate window:

Imports System.Diagnostics
...
Dim stack_trace As New System.Diagnostics.StackTrace(True)
Debug.WriteLine(stack_trave.ToString())

The StackTrace class also provides methods for exploring call frames in the stack. The FrameCount property and the GetFrame and GetFrames methods give you access to StackFrame objects representing the frames. StackFrame objects provide some additional detail not listed by the StackTrace object's ToString method such as each code's file name, line number, and column number. Example program ClimbStackTrace, which is available for download on the book's web site, shows how a program can climb through the layers of a stack trace and display information about each call level.

Throwing Exceptions

In addition to catching exceptions, your program may need to generate its own exceptions. Because handling an exception is called catching it, raising an exception is called throwing it. (This is just a silly pun. People also catch lions and colds, but I don't think many people throw them. It's as good a term as any, however.)

To throw an error, the program creates an instance of the type of exception it wants to generate, passing the constructor additional information describing the problem. The program can set other exception fields if you like. For example, it might set the exception's Source property to tell any other code that catches the error where it originated. The program then uses the Throw statement to raise the error. If an error handler is active somewhere in the call stack, Visual Basic jumps to that point and the error handler processes the exception.

Example program DrawableRect, which is available for download on the book's web site, uses the following code to show how the DrawableRectangle class can protect itself against invalid input:

Public Class DrawableRectangle
      Public Sub New(ByVal new_x As Integer, ByVal new_y As Integer,
      ByVal new_width As Integer, ByVal new_height As Integer)
         ' Verify that new_width > 0.
         If new_width <= 0 Then
             Dim ex As New ArgumentException(
                 "DrawableRectangle must have a width greater than zero",
                  "new_width")
           Throw ex
         End If

         ' Verify that new_height> 0.
         If new_height < = 0 Then
             Throw New ArgumentException(
                  "DrawableRectangle must have a height greater than zero",
                  "new_height")
         End If
         ' Save the parameter values.
         ...
    End Sub
    ...
End Class
                                                  
Throwing Exceptions

The class's constructor takes four arguments: an X and Y position, and a width and height. If the width is less than or equal to zero, the program creates a new ArgumentException object. It passes the exception's constructor a description string and the name of the argument that is invalid. After creating the exception object, the program uses the Throw statement to raise the error. The code checks the object's new height similarly, but it creates and throws the exception in a single statement to demonstrate another style for throwing an error.

The following code shows how a program might use a Try block to protect itself while creating a new DrawableRectangle object:

Try
    Dim rect As New DrawableRectangle(10, 20, 0, 100)
Catch ex As Exception
    MessageBox.Show(ex.Message)
End Try

When your application needs to throw an exception, it's easiest to use an existing exception class. There are a few ways to get lists of exception classes so that you can find one that makes sense for your application. First, Appendix O, "Useful Exception Classes," lists some of the more useful exception classes. The online help topic, "Introduction to Exception Handling in Visual Basic .NET" at msdn.microsoft.com/aa289505.aspx also has a good list of exception classes at the end. Microsoft's web page msdn.microsoft.com/system.exception_derivedtypelist.aspx provides a very long list of exception classes that are derived from the System.Exception class.

Another method for finding exception classes is to open the Object Browser (select the View menu's Object Browser command) and search for "Exception." Figure 19-2 shows the Object Browser displaying roughly 400 matches, many of which are exception classes. The System.FormatException class is selected, so the Object Browser is showing that class's description.

You can use the Object Browser to find exception classes.

Figure 19.2. You can use the Object Browser to find exception classes.

When you throw exceptions, you must use your judgment about selecting these classes. For example, Visual Basic uses the System.Reflection.AmbiguousMatchException class when it tries to bind a subroutine call to an object's method, and it cannot determine which overloaded method to use. This happens at a lower level than your program will act, so you won't use that class for exactly the same purpose. It may be useful, for example, if your routine parses a string and, based on the string, cannot decide what action to take. In that case, you might use this class to represent the error, even though you're not using it exactly as it was originally intended.

Be sure to use the most specific exception class possible. Using more generic classes such as Exception makes it much harder for developers to understand and locate an error. If you cannot find a good, specific fit, create your own exception class as described in the section "Custom Exceptions" later in this chapter.

Before you use one of these classes, look it up in the online help to make sure that it fits your purpose. If there's no good fit, you can always create your own as described in the following section, "Custom Exceptions."

Specialized classes and libraries sometimes have their own particular exception classes. For example, serialization and cryptographic objects have their own sets of exception classes that make sense within their own domains. Usually, these are fairly specialized, so you won't need to throw them in your program unless you are re-raising an error you received from a serialization or cryptographic object.

Re-throwing Exceptions

Sometimes when you catch an exception, you cannot completely handle the problem. In that case, it may make sense to re-throw the exception so call higher up in the call stack can take a crack at it.

To re-throw an error exactly as you caught it, simply use the Throw keyword as in the following example.

Try
    ' Do something hard here.
    ...

Catch ex As ArithmeticException
    ' We can handle this exception. Fix it.
    ...

Catch ex As Exception
    ' We don't know what to do with this one. Re-throw it.
    Throw
End Try

If your code can figure out more or less why an error is happening but it cannot fix it, it's often a good idea to re-throw the error as a different exception type. For example, suppose a piece of code causes an ArithmeticException but the underlying cause of the exception is an invalid argument. In that case it is better to throw an ArgumentException instead of an ArithmeticException because that will provide more specific information higher up in the call stack.

At the same time, however, you don't want to lose the information contained in the original ArithmeticException.

The solution is to throw a new ArgumentException but place the original ArithmeticException in its InnerException property so code that catches the new exception has access to the original information.

The following code demonstrates this technique:

Try
    ' Do something hard here.
    ...

Catch ex As ArithmeticException
    ' This was caused by an invalid argument.
    ' Re-throw it as an ArgumentException.
    Throw New ArgumentException("Invalid argument X in function Whatever.", ex)

Catch ex As Exception
    ' We don't know what to do with this one. Re-throw it.
    Throw
End Try

Custom Exceptions

When your application needs to raise an exception, it's easiest to use an existing exception class. Reusing existing exception classes makes it easier for developers to understand what the exception means. It also prevents exception proliferation, where the developer needs to watch for dozens or hundreds of types of exceptions.

Sometimes, however, the predefined exceptions don't fit your needs. For example, suppose that you build a class that contains data that may exist for a long time. If the program tries to use an object that has not refreshed its data for a while, you want to raise some sort of "data expired" exception. You could squeeze this into the System.TimeoutException class, but that exception doesn't quite fit this use. The Expired class is a better fit, but it's part of the System.Net.Cookie namespace. Using it would require your application to include the System.Net.Cookie namespace just to define the exception class, even if the program has nothing to do with cookies. In this case, it would probably be better to create your own exception class.

Building a custom exception class is easy. Make a new class that inherits from the System.ApplicationException class. Then, provide constructor methods to let the program create instances of the class. That's all there is to it.

By convention, an exception class's name should end with the word Exception. Also by convention, you should provide at least three overloaded constructors for developers to use when creating new instances of the class. (For more information on what constructors are and how to define them, see the section "Class Instantiation Details" in Chapter 26, "Classes and Structures.")

The first constructor takes no parameters and initializes the exception with a default message describing the general type of error.

The other two versions take as parameters an error message, and an error message plus an inner exception object. These constructors pass their parameters to the base class's constructors to initialize the object appropriately.

For completeness, you can also make a constructor that takes as parameters a SerializationInfo object and a StreamingContext object. This version can also pass its parameters to a base class constructor to initialize the exception object, so you don't need to do anything special with the parameters. This constructor is useful if the exception will be serialized and deserialized. If you're not sure whether you need this constructor, you probably don't. If you do include it, however, you will need to import the System.Runtime.Serialization namespace in the exception class's file to define the SerializationInfo and StreamingContext classes.

Example program CustomException uses the following code to define the ObjectExpiredException class:

Imports System.Runtime.Serialization

Public Class ObjectExpiredException
    Inherits System.ApplicationException

    ' No parameters. Use a default message.
Public Sub New()
        MyBase.New("This object has expired")
    End Sub

    ' Set the message.
    Public Sub New(ByVal new_message As String)
        MyBase.New(new_message)
    End Sub

    ' Set the message and inner exception.
    Public Sub New(ByVal new_message As String,
    ByVal inner_exception As Exception)
        MyBase.New(new_message, inner_exception)
    End Sub

    ' Include SerializationInfo object and StreamingContext objects.
    Public Sub New(ByVal info As SerializationInfo,
    ByVal context As StreamingContext)
        MyBase.New(info, context)
    End Sub
End Class
                                                  
Custom Exceptions

After you have defined the exception class, you can throw and catch it just as you can throw and catch any exception class defined by Visual Basic. For example, the following code throws an ObjectExpiredException error:

Throw New ObjectExpiredException("This Customer object has expired.")

The parent class System.ApplicationException automatically handles the object's Message, StackTrace, and ToString properties so you don't need to implement them yourself.

VISUAL BASIC CLASSIC ERROR HANDLING

Structured error handling using the Try block is a relatively recent innovation, appearing in the first versions of Visual Basic .NET. Visual Basic 6 and earlier versions used a more line-oriented syntax sometimes called Visual Basic Classic Error Handling. Although the Try block is generally preferred, you can still use classic error handling in your Visual Basic .NET applications. In fact, you can use both styles in the same program, although not in the same routine. The section "Structured versus Classic Error Handling" later in this chapter discusses the pros and cons of each.

A classic error handler begins with an On Error statement that tells Visual Basic what it should do if it encounters an error. This statement can take one of four forms: On Error GoTo line, On Error Resume Next, On Error GoTo 0, and On Error GoTo −1.

On Error GoTo Line

After the On Error GoTo line statement, if Visual Basic encounters an error, it enters error-handling mode and jumps to the indicated line. The error handler that begins at the indicated line can take whatever action is appropriate.

The following code executes the statement On Error GoTo LoadPayrollError and then calls subroutine LoadPayrollFile. If that routine causes an error, Visual Basic jumps to the line labeled LoadPayrollError. The error-handling code displays a message and exits the subroutine. The program then executes the statement On Error GoTo PrintPaychecksError and calls the PrintPaychecks routine. If that routine throws an error, the code starting at the PrintPaychecksError label executes. After it has finished its work, the routine uses an Exit Sub statement to end without falling into the error-handling code that follows.

Private Sub ProcessPayroll()
    ' Load the payroll file.
    On Error GoTo LoadPayrollError
    LoadPayrollFile()

    On Error GoTo PrintPaychecksError
    ' Print paychecks.
    PrintPaychecks()

    ' We're done.
    Exit Sub

LoadPayrollError:
    MessageBox.Show("Error loading the payroll file.")
    Exit Sub

PrintPaychecksError:
    MessageBox.Show("Error printing paychecks.")
    Exit Sub
End Sub

The program can leave error-handling mode using the statements Exit Sub, Exit Function, Exit Property, Resume, or Resume Next.

An Exit Sub, Exit Function, or Exit Property statement makes the program immediately leave the routine in which the error occurred, and that's the end of error-handling mode for this error.

The Resume statement makes the program resume execution with the statement that caused the error. If the problem has not been fixed, the error will occur again and the program may enter an infinite loop. You should use the Resume statement only if there is a chance that the error has been fixed. For example, if the program tries to read from a floppy disk and the drive is empty, the program could ask the user to insert the disk and then it could try to read the disk again.

The Resume Next statement makes the program resume execution with the statement after the one that caused the error. This statement is appropriate when the program cannot fix the problem but should continue anyway. For example, suppose that a program fails to read a value from a file. It might want to continue anyway so that it can close the file in the next statement.

On Error Resume Next

After the On Error Resume Next statement, if Visual Basic encounters an error, it skips the statement that caused the error and resumes execution with the following statement. If the program doesn't care whether the statement completed, On Error Resume Next lets it continue without checking for errors.

If the program needs to take action when an error occurs, it can use the Err object to check for errors after each statement. For example, the following code uses the On Error Resume Next statement and then calls subroutine DoSomething. When the subroutine returns, the program checks the Err object's Number property to see if an error occurred. If there is an error, the program displays a message and exits the subroutine. If subroutine DoSomething did not cause an error, the program calls subroutine DoSomethingElse and performs a similar check for errors.

On Error Resume Next
DoSomething()
If Err.Number <> 0 Then
    MessageBox.Show("Error in DoSomething")
    Exit Sub
End If

DoSomethingElse()
If Err.Number <> 0 Then
    MessageBox.Show("Error in DoSomethingElse")
    Exit Sub
End If
    ...

A program can also use this statement to check for different kinds of errors and take appropriate action. The following example takes no special action if there is no error. If Err.Number is 11, the program tried to divide by zero. In that case, the code sets variable X to a default value. If there is some other error, the program tells the user and exits the subroutine.

' Try to calculate X.
On Error Resume Next
X = CalculateValue()
Select Case Err.Number
    Case 0 ' No error. Do nothing.
    Case 11 ' Divide by zero. Set a default value.
        X = 1000
    Case Else ' Unexpected error. Tell the user.
        MessageBox.Show("Error calculating X." & vbCrLf & Err.Description)
        Exit Sub
End Select
    ...

On Error GoTo 0

The On Error GoTo 0 statement disables any active error handler. You should deactivate an error handler when it no longer applies to what the program is doing. The following code installs an error handler while it loads some data. When it is finished loading the data, it uses On Error GoTo 0 to deactivate the error handler before it performs other tasks.

On Error GoTo LoadDataError
    ' Load the data.
    ...
    ' Done loading data.
    On Error GoTo 0
    ...
    Exit Sub

LoadDataError:
    MessageBox.Show("Error loading data." & vbCrLf & Err.Description)
    Exit Sub
End Sub

Deactivating the error handler stops the program from taking inappropriate action for an error. In the preceding example, it might confuse the user to say there was an error loading data when the program was doing something else. In other cases, the program might incorrectly try to fix problems that are not there if you leave an old error handler installed. For example, the program might ask the user to insert a floppy disk when it had already finished reading from the disk.

Deactivating old error handlers also lets the program fail if an unexpected error occurs. That lets developers discover and handle new types of failure, possibly by adding a new error handler.

On Error GoTo −1

The On Error GoTo −1 statement is very similar to On Error GoTo 0. It deactivates any active error handler. However, it also ends error-handling mode if it is running. Example program OnErrorGoToMinus1 uses the following code to show the difference:

Dim i As Integer
Dim j As Integer = 0

    On Error GoTo DivideError1
    i = 1  j ' This raises an error.

DivideError1: ' We enter error-handling mode here.
    On Error GoTo −1 ' This ends error-handling mode.
    On Error Resume Next ' Ignore errors in the future.
    i = 1  j ' This error is ignored.
    Exit Sub

    On Error GoTo DivideError2
    i = 1  j ' This raises an error.
    Exit Sub

DivideError2: ' We enter error-handling mode here.
    On Error GoTo 0 ' This does NOT end error-handling mode.
    On Error Resume Next ' Doesn't work in error-handling mode.
    i = 1  j ' This error is not caught and crashes the program.
    Exit Sub
                                                  
On Error GoTo −1

The program uses On Error GoTo DivideError1 to install an error handler and then executes a command that causes a divide-by-zero error.

The error-handling code uses On Error GoTo −1 to end error-handling mode and continue execution. It then calls On Error Resume Next to ignore further errors and performs another calculation that divides by zero. Because the On Error Resume Next statement is in effect, the program ignores this error.

Next, the code uses On Error GoTo DivideError2 to install another error handler. It divides by zero again to jump to the error handler and enter error-handling mode.

This time, the error handler uses the On Error GoTo 0 statement. This uninstalls the current error handler (On Error GoTo DivideError2) but does not end error-handling mode. The program then uses the On Error Resume Next statement. Unfortunately, this statement is ignored while the program is running in error-handling mode. If the program used a Resume statement to exit error-handling mode, this statement would then have an effect, but it does nothing until error-handling mode ends. Now, when the program divides by zero again, there is no active error handler, so the program crashes.

To avoid confusion, you should not use this style of error handling with error-handling code running through the body of a routine. Instead, place error-handling code at the end of the routine and use Exit Sub, Exit Function, Exit Property, Resume, or Resume Next to return to the routine's main body of code. The On Error GoTo −1 statement is usually more confusing than it's worth.

Error-Handling Mode

Undoubtedly, the most confusing part of classic error handling is error-handling mode. The On Error GoTo line statement makes the program enter a special error-handling mode that remains in effect until the error handler calls Exit Sub, Exit Function, Exit Property, Resume, Resume Next, or On Error GoTo −1.

While in error-handling mode, most other error-handling statements do not work as they normally do. Generally, their effects only take place when error-handling mode finally ends. In the example in the previous section, the final On Error Resume Next statement has no effect because it executes while the program is in error handling mode.

Trying to execute error-handling statements within error handling mode is one of the most common mistakes programmers make when working with error-handling mode. The error-handling code must be safe, or the program will crash (or at least the error will propagate up to the calling routine).

If you really need to perform operations that might crash within the error handler's code, move that code into a subroutine. That routine can use its own error-handling code to protect itself from another error. The following example demonstrates this approach. The SetDefaultValue subroutine uses its own On Error Resume Next statement to avoid crashing if it has problems of its own.

Private i, j As Integer

Private Sub PerformCalculation()
    On Error GoTo EquationError
    i = 1  j
    Exit Sub

EquationError:
    SetDefaultValue()
    Resume Next
End Sub

Private Sub SetDefaultValue()
    On Error Resume Next
    i = 2  j
End Sub

STRUCTURED VERSUS CLASSIC ERROR HANDLING

The newer structured error-handling approach provided by the Try statement has several advantages over classic error handling. First, classic error handling doesn't make it immediately obvious whether a piece of code is protected by an error handler. To determine whether a statement is protected, you must look back through the code until you find an On Error statement. If you come to a labeled line, you also must track down any places where a GoTo or a Resume line statement could jump to that line and see what error handler might be installed at the time.

Classic error handling also doesn't make it obvious whether the code is running in error-handling mode. In some cases, it is impossible to tell until runtime. The following code uses an On Error GoTo statement to protect itself and then initializes an integer from a value that the user enters in a text box. If the user enters a valid integer, the code works normally and keeps running in normal (not error-handling) mode. If the user enters a value that is not a valid integer, the program jumps to the label BadFormat and enters error-handling mode. There's no way to tell before runtime whether the program will be in error-handling mode when it reaches the following comment.

Dim i As Integer
    On Error GoTo BadFormat
    i = CInt(txtNumber.Text)
BadFormat:
    ' Are we in error-handling mode here?
    ...

Finally, you cannot nest classic error-handling code. If you must perform a risky action in an error handler, you must place the code in a separate subroutine that contains its own error-handling code to protect itself.

Structured error handling addresses these shortcomings. By looking at the enclosing Try or Catch block, you can easily tell whether a line of code is protected (inside the Try block) or part of an error handler (in the Catch block).

You can even nest Try statements, as shown in the following code. The program tries to initialize an integer from a value that the user entered in a text box. If the user enters an invalid value, the code moves into the first Catch block. There it tries to set the value of the integer using a calculation. If that calculation fails (for example, if j is 0), the next Catch block sets the variable to a default value.

' Get the user's value.
Try
    i = Integer.Parse(txtNumber.Text)
Catch ex As Exception
    ' The user's value is no good.
    ' Calculate a different value.
    Try
        i = 1  j
    Catch ex2 As Exception
       ' The calculated value is no good.
       ' Use a default value.
       i = 3
    End Try
End Try

Finally, the Try block doesn't have a bewildering error-handling mode. The potential for confusion there alone is probably worth using structured error handling.

One of the few advantages to classic error handling is that it is easier to ignore errors by using the On Error Resume Next statement. The following code uses classic error handling to execute three subroutines and ignore any errors they produce:

On Error Resume Next
DoSomething()
DoSomethingElse()
DoSomethingMore()
...

The following version shows the same code using structured error handling. This version is quite a bit more verbose and much less readable.

Try
    DoSomething()
Catch
End Try

Try
    DoSomethingElse()
Catch
End Try

Try
    DoSomethingMore()
Catch
End Try
...

THE ERR OBJECT

When an error occurs, Visual Basic initializes an object named Err. You can use this object's properties to learn more about the error. These properties correspond to those provided by the exception objects used by the Try statement's Catch sections. The following table lists these properties.

PROPERTY

PURPOSE

Description

A message describing the error.

Erl

The line number at which the error occurred.

HelpContext

The help context ID for the error.

HelpFile

The full path to the help file describing the error.

LastDLLError

A system error code generated by a call to a DLL (if appropriate).

Number

The error number. The value 0 means no error has occurred.

Source

The name of the object or application that caused the error.

The Err object also provides three useful methods for working with errors: Clear, Raise, and GetException. The Clear method clears the object's information and resets it for the next statement. If the statement following an error does not raise an error itself, the Err object may still show the previous error unless you clear it, as shown in the following code:

On Error Resume Next
X = Single.Parse(txtX.Text)
If Err.Number <> 0 Then
    MessageBox.Show(Err.Description) ' Display the error.
    Err.Clear ' Clear the error.
End If

Y = Single.Parse(txtY.Text)
If Err.Number <> 0 Then
    MessageBox.Show(Err.Description) ' Display the error.
    Err.Clear ' Clear the error.
End If
...

The Err object's Raise method generates an error. For example, the following statement raises error number 5, "Procedure call or argument is invalid":

Err.Raise(5)

Finally, the GetException method returns an Exception object representing the Err object's error. You can use this object just as you can use any other exception object. In particular, you can use its StackTrace property to get a trace showing where the error occurred.

If you use classic error handling, you can use the Err object to learn about the error. If you use structured error handling with the Try statement, you can use the Exception objects provided by Catch statements, and you can do without the Err object.

DEBUGGING

Visual Basic provides a rich set of tools for debugging an application. Using the development environment, you can stop the program at different lines of code and examine variables, change variable values, look at the call stack, and call routines to exercise different pieces of the application. You can step through the program, executing the code one statement at a time to see what it is doing. You can even make some modifications to the source code and let the program continue running.

Chapter 7, "Debugging," describes tools that the development environment provides to help you debug an application. These include tools for stepping through the code, breakpoints, and windows such as the Immediate, Locals, and Call Stack windows. See Chapter 7 for details.

In addition to setting breakpoints in the code, you can use the Stop statement to pause execution at a particular line. This can be particularly useful for detecting unexpected values during testing. For example, the following statement stops execution if the variable m_NumEmployees is less than 1 or greater than 100:

If (m_NumEmployees < 1) Or (m_NumEmployees > 100) Then Stop

SUMMARY

In practice, it's extremely difficult to anticipate every condition that can occur within a large application. You should try to predict as many incorrect situations as possible, but you should also plan for unforeseen errors. You should write error-checking code that makes bugs obvious when they occur and recovers from them if possible. You may not be able to anticipate every possible bug, but with a little thought you can make the program detect and report obviously incorrect values.

You should also look for unplanned conditions (such as the user entering a phone number in a Social Security number field) and make the program react gracefully. Your program cannot control everything in its environment (such as the user's actions, printer status, and network connectivity), but it should be prepared to act when things aren't exactly the way they should be.

When you do encounter an error, you can use tools such as breakpoints, watches, and the development environment's Locals, Auto, Immediate, and Call Stack windows to figure out where the problem begins and how to fix it. You may never be able to remove every last bug from a 100,000-line program, but you can make any remaining bugs appear so rarely that the users can do their jobs in relative safety.

Chapters 8 through 13 focus on controls, forms, and other user interface objects. Chapters 14 through 18 move the focus to the code that lies behind the user interface. Chapter 20, "Database Controls and Objects," covers database topics that fall into both the user interface and non-user interface categories. It describes database controls that you can use to build an application's user interface as well as components and other objects that you can use behind the scenes to manipulate databases.

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

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