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 in the code 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 you can take some proactive steps to make these sorts of bugs easier to find.


BUGS THROUGHOUT HISTORY
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 1947. For a bit more information, including a picture of this first computer bug, see http://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’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.

You can’t fix unplanned conditions but you can try to make your program handle them gracefully and produce some meaningful result instead of crashing.

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 5,000-page invoice with no shipping or billing address. Only then would the developers realize that there was a problem.

Rather than trying to work around the problematic data, it would be better if the sorting routine immediately told developers that something was wrong so they could 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.


AUTOMATED BUG CATCHERS
Many companies 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 automatically 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, which is available for download from the book’s website, 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.
    For i As Integer = 2 To the_order.Items.Count
        Dim order_item1 = the_order.Items(i - 1)
        Dim order_item2 = the_order.Items(i)
        Debug.Assert(order_item1.Price <= order_item2.Price,
            "Order items not properly sorted")
    Next i
#End If
End Sub

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 it, 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 often predict where one 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 flash drive from the drive before the program is ready or unplug the printer while your program is using it.

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 ahead of time before you perform an action that might fail rather than simply attempting to perform the action and then catching the error when you fail. Testing for problem conditions generally gives you more complete information about what’s wrong. It’s also usually faster than catching an error because 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 one. 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

The code checks that the field is not blank and uses the IsNumeric function to verify that the field contains a 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.


NOTE
These checks must always occur so you cannot replace them with Debug.Assert statements, which are removed from release builds.

A typical program might need to read and validate many values, and retyping this code for each value 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 that kind of 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.


ERRORS, ERRORS, EVERYWHERE
In fact, some sources of errors are completely beyond your control. For example, power surges, static electricity, intermittent short circuits, or even stray radiation striking exactly the right part of a chip can make the computer’s hardware misbehave so code that should work correctly fails. There’s little you can do to anticipate these kinds of errors but you can use global error handling to try to recover from them.
Of course that doesn’t excuse you from rigorously checking your code for errors. The vast majority of bugs are due to real mistakes in the code or data rather than to magical cosmic rays flipping a single bit on a memory chip.

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.


KEEP RUNNING
Usually it’s better for an application to do the best it can to recover and keep running instead of exiting. Even if the program must reset itself to a default state, that at least saves the user the trouble of restarting the application, reopening forms, arranging toolbars, and otherwise getting the program ready to work. Before you decide, compare the difficulty of making the program reset and continue with the trouble the user will have restarting and getting back to work.

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(sender As Object,
 e As ApplicationServices.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, so the UnhandledException event handler never executes. If you run the compiled executable, however, the UnhandledException event fires and the global error handler runs.

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

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