Chapter 8: 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 didn’t think 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 advanced 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 condition 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.

Tip 

On a historical note, the term bug has been used since at least the time of the telegraph to mean some sort of defect. Probably the origin of the term in computer science was an actual moth that was caught between two relays in an early computer in 1945. For a bit more information, including a picture of this first computer bug, see www.jamesshuggins.com/h/tek1/first_computer_bug.htm.

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 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 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 for the sorting routine to immediately tell developers that something is wrong so that 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 their 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. Since it’s difficult for any nontrivial program to anticipate every possible condition, you cannot assume that all the routines are perfect and that the data remains uncorrupted.

Tip 

Many companies these days use automated testing tools to try to flush out problems early. Regression testing tools can execute code to verify that its outcome isn’t changed after you have made modifications to other parts of the application. If you build a suite of testing routines to validate data and subroutines’ results, you may be able to work them into an automated testing system, too.

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.

The following subroutine starts by validating its input. It verifies that the Order object it is passed has an Items collection and that its Customer variable 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.

  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  

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 Unexpected Conditions

Although you don’t want an unexpected condition to happen, with some careful thought, you can predict where an unexpected 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 the wrong 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 unexpected 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 unexpected 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 zero or a negative number when you are expecting a positive number. The user may even leave the field blank.

The following code shows an improved version of this declaration. It 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. Because IsNumeric doesn’t exactly match Integer.Parse, the program still needs to use a Try Catch block 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.

  ' 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 

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 the function shown in the following code:

  ' If the TextBox doesn't contain an integer ' display an error message, set focus to the TextBox, and return False. ' Otherwise return True and return the integer through the ByRef ' parameter result. Private Function IsValidInteger(ByRef result As Integer, _  ByVal txt As TextBox, ByVal field_name As String, _  Optional ByVal min_value As Integer = Integer.MinValue, _  Optional ByVal max_value As Integer = Integer.MaxValue) As Boolean     ' Check for blank entry.     Dim num_items_txt As String = txt.Text     If num_items_txt.Length < 1 Then         MessageBox.Show("Please enter " & field_name & ".")         txt.Focus()         Return False     End If     ' See if it's numeric.     If Not IsNumeric(num_items_txt) Then         MessageBox.Show(field_name & " must be a number.")         txt.Select(0, num_items_txt.Length)         txt.Focus()         Return False     End If     ' Assign the value.     Try         result = Integer.Parse(txt.Text)     Catch ex As Exception         MessageBox.Show("Error in " & field_name & "." & _             vbCrLf & ex.Message)         txt.Select(0, num_items_txt.Length)         txt.Focus()         Return False     End Try     ' Check that the value is between min_value and max_value.     If result < min_value Or result > max_value Then         MessageBox.Show(field_name & " must be between " & _             min_value.ToString & " and " & max_value.ToString & ".")         txt.Select(0, num_items_txt.Length)         txt.Focus()         Return False     End If     ' The value is okay.     Return True End Function  

Now the program can use this function, as in the following code:

  Dim num_items As Integer If Not IsValidInteger(num_items, txtNumItems, "Num Items", 1, 100) Then Exit Sub ... 

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 Visual Basic 2005, 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 such as this one, 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, StartupNext?Instance, and UnhandledException. Select the last of these commands to open the Unhandled?Exception event handler.

In the event handler, you can take whatever action is appropriate for the error. Since 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.

The following code displays 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 _  System.Windows.Forms.UnhandledExceptionEventArgs) _  Handles Me.UnhandledException     MessageBox.Show("Exception caught globally" & vbCrLf & _         e.Exception.Message)     e.ExitApplication = False End Sub 

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




Visual Basic 2005 with  .NET 3.0 Programmer's Reference
Visual Basic 2005 with .NET 3.0 Programmer's Reference
ISBN: 470137053
EAN: N/A
Year: 2007
Pages: 417

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net