When an error occurs, Visual Basic looks for an active error handler in the currently executing routine.. Control continues moving up the call stack until Visual Basic finds an active er
Trang 1Bug Proofing
Any non-trivial application is extremely likely to contain bugs If you write more than a few hun-dred lines of code, bugs are practically inevitable Even after a program is thoroughly tested and has been in use for awhile, it probably still contains bugs waiting to appear later
Although bugs are nearly guaranteed, you can take steps to minimize their impact on the applica-tion Careful design and planning can reduce the total number of bugs that are introduced into the code to begin with “Offensive programming” techniques that emphasize bugs rather than hiding them and verified design by contract (DBC) can detect bugs quickly after they are introduced Thorough testing can detect bugs before they affect customers
Together, these techniques can reduce the probability of a user finding a bug to an extremely small level No matter how small that probability, however, users are likely to eventually stumble across the improbable conditions that bring out the bug
This chapter discusses some of the techniques you can use to make an application more robust in the face of bugs It shows how a program can detect bugs when they reveal themselves at run-time, and explains the actions that you might want the program to take in response
Catching Bugs Suppose you have built a large application and tested repeatedly until you can no longer find any bugs Chances are there are still bugs in the code; you just haven’t found them yet Eventually a user will load the right combination of data, perform the right sequence of actions, or use up the right amount of memory with other applications and a bug will appear Just about any resource that the program needs and that lies outside of your code can cause problems that are difficult to predict
Trang 2Modern networked applications have their own whole set of unique problems If the network is heavily loaded, requests may time out If a network resource such as a Web site or a Web Service is unavailable
or just plain broken, the program won’t be able to use it These sorts of situations can be quite difficult to test How do you simulate a heavy load or an incorrect response from the Google or TerraServer Web Services?
When that happens, how does the program know that a bug has occurred? Many applications have no idea when an error is occurring They obliviously corrupt the user’s data or display incorrect results They continue blithely grinding the user’s data into garbage, if they don’t crash outright Only the user can tell if a bug occurred, and, if the bug is subtle, the user may not even notice
There are two ways a program can detect bugs: it can wait for bugs to come to it or it can go hunting for bugs
Waiting for Bugs
One way to detect bugs is to wait for a bug that is so destructive that it cannot be ignored These bugs are so severe that the program must handle them or crash They include such errors as division by zero, accessing array entries that don’t exist, invoking properties and methods of objects that are not allocated, trying to read beyond the end of a file, and trying to convert an object into an incompatible object type
A program can protect against these kinds of bugs by surrounding risky code with a Try Catchblock Experience and knowledge of the kinds of operations that the code performs tell you where you need to put this kind of error trapping For example, if the program performs arithmetic calculations that might divide by zero or tries to open a file that may not exist, the program needs protection
But it is assumed that you cannot know exactly every place that an error might occur After all, if you could predict every possible error, you could protect against them and there would be no problem So, how can you trap every conceivable error? The answer lies in how Visual Basic handles errors
When an error occurs, Visual Basic looks for an active error handler in the currently executing routine If there is no active Try Catchblock or On Errorstatement, control moves up the call stack to the routine that called this one Visual Basic then looks for an active error handler at that level If that routine also does not have an active error handler, control moves up the call stack again
Control continues moving up the call stack until Visual Basic finds an active error handler, or until con-trol pops off the top of the stack and the program crashes At that point, Visual Basic deals with the error
by displaying a usually cryptic error message and then sweeping away the program’s wreckage If an error handler catches the error at any time while climbing up the call stack, the program can continue running One way you can be certain to catch all errors is to put Try Catchblocks around every routine that might be at the top of the call stack
Because Visual Basic is event-driven, there are only two kinds of routines that can start code running, and that can be at the top of the call stack: event handlers and Sub Main
That observation leads to a way for catching every possible error: put a Try Catchblock around every event handler and Sub Main(if it exists) Now, any time a bug rears its ugly head, the routine at the top
of the call stack catches the error in its Try Catchblock and saves the program from crashing
Trang 3Example program CrashProofuses the following code to protect three event handlers from crashing The code in each event handler is contained in a Try Catchblock If the code fails, the program displays
an error message and continues running
‘ Cause a divide by zero error
Private Sub btnDivideByZero_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnDivideByZero.Click Try
Dim i As Integer = 1 Dim j As Integer = 0
i = i \ j Catch ex As Exception MessageBox.Show(“Error performing calculation” & _ vbCrLf & ex.Message, “Calculation Error”, _ MessageBoxButtons.OK, MessageBoxIcon.Exclamation) End Try
End Sub
‘ Cause an error by trying to read a missing file
Private Sub btnOpenMissingFile_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnOpenMissingFile.Click Try
Dim txt As String = My.Computer.FileSystem.ReadAllText( _
“Q:\Missing\missingfile.xyz”) Catch ex As Exception
MessageBox.Show(“Error reading file” & _ vbCrLf & ex.Message, “File Error”, _ MessageBoxButtons.OK, MessageBoxIcon.Exclamation) End Try
End Sub
‘ Cause an error by accessing an out of bounds index
Private Sub btnIndexError_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnIndexError.Click Try
Dim values(0) As Integer values(1) = 1
Catch ex As Exception MessageBox.Show(“Error setting array value” & _ vbCrLf & ex.Message, “Array Error”, _ MessageBoxButtons.OK, MessageBoxIcon.Exclamation) End Try
End Sub
The CrashProofprogram (available for download at www.vb-helper.com/one_on_one.htm) also contains unprotected versions of these event handlers so that you can see what happens if the Try Catchblocks are missing
All of this leads to a common development strategy:
❑ Start by writing code with little or no error handling
❑ Add Try Catchblocks in places where you expect errors to occur This is usually where the pro-gram interacts with some external entity such as a user, file, or Web Service that might return an
Trang 4invalid result These are really not bugs in the sense that the code is doing the wrong thing Instead, it is where invalid interactions with external systems lead to bad behavior
❑ Test the application Whenever you encounter a new bug, add appropriate error handling
At this point, some developers declare the application finished and ship it More-thorough developers add Try Catchblocks to every event handler and Sub Mainthat doesn’t already contain error handling
to make the application crash-proof
Global Error Handling
One problem with this wait-for-the-bug technique is that it requires that you add a lot of error-handling code when you don’t know that an error might occur Not only is that a lot of work, but it also makes the code more cluttered and more difficult to read
To help with this problem, some Visual Basic 6 products could automatically add error handling to
every routine that did not already have it You would write code to protect against predictable errors
such as invalid inputs and missing files, and then the product would automatically protect every other routine in the application.
Visual Basic 2005 helped with this problem by adding the ability to make an application-level error han-dler Instead of adding a Try Catchblock to every unprotected event handler and Sub Main, you can create a single event handler to catch unhandled exceptions
To do that, open Solution Explorer and double-click My Project Scroll to the bottom of the application’s property page and click the View Application Events button shown in Figure 16-1
Figure 16-1: Use the View Application Events button to catch unhandled errors
Trang 5This opens a code editor for application-level event handlers In the code editor’s left drop-down, select
“(MyApplication Events).” Then in the right drop-down, select UnhandledException Now you can add code to handle any exceptions that are not caught by Try Catchblocks elsewhere in your code The following code shows the application module containing an UnhandledExceptionevent handler with some automatically generated comments removed to make the code easier to read:
Namespace My Partial Friend Class MyApplication Private Sub MyApplication_UnhandledException(ByVal sender As Object, _ ByVal e As Microsoft.VisualBasic.ApplicationServices _
UnhandledExceptionEventArgs) Handles Me.UnhandledException MessageBox.Show(“Unexpected error” & _
vbCrLf & vbCrLf & e.Exception.Message & _ vbCrLf & vbCrLf & e.Exception.StackTrace.ToString(), _
“Unexpected Error”, _ MessageBoxButtons.OK, _ MessageBoxIcon.Exclamation) e.ExitApplication = False End Sub
End Class End Namespace
The event handler code displays a message box showing the exception’s message and a stack trace It then sets e.ExitApplicationto Falseso that the program continues running
Now, the program is “crash-proof,” but you don’t need to clutter the code with a huge number of pre-cautionary Try Catchblocks
Note that the event handler must itself be crash-proof If the UnhandledExceptionevent handler throws an exception, the program crashes Use Try Catchblocks to protect the event handler.
This technique is fairly effective, but it has some drawbacks First, by preventing the application from exiting, this particular example can lead to an infinite loop When the program fails to crash, it may execute the same code that caused the initial problem The program may become trapped in a loop throwing an unexpected error, catching it in the UnhandledExceptionerror handler, setting
e.ExitApplicationto False, and then throwing the same error again
A second problem is that the UnhandledExceptionevent handler is normally disabled when a debug-ger is attached to the program, as is normally the case when you run in the Visual Basic IDE That makes
it easier to find and handle new bugs You run the program in the IDE and, when a bug occurs, you can study it in the debugger and add code to handle the situation properly
Unfortunately, this also makes testing the UnhandledExceptionevent handler more difficult because errors won’t invoke this routine in the IDE One way to test your global error-handling code is to move it into another routine that you can then call directly
The following code shows how you can rewrite the previous example The UnhandledExceptionevent handler simply calls subroutine ProcessUnhandledException, which does all the work Both of these routines are contained in the MyApplicationclass inside the ApplicationEvents.vbmodule where you would normally create the UnhandledExceptionevent handler
Trang 6Private Sub MyApplication_UnhandledException(ByVal sender As Object, _
ByVal e As Microsoft.VisualBasic.ApplicationServices _
UnhandledExceptionEventArgs) Handles Me.UnhandledException
ProcessUnhandledException(sender, e) End Sub
‘ Deal with an unhandled exception
Public Sub ProcessUnhandledException(ByVal sender As Object, _
ByVal e As Microsoft.VisualBasic.ApplicationServices _
UnhandledExceptionEventArgs)
e.ExitApplication = _ MessageBox.Show(“Unexpected error End application?” & _ vbCrLf & vbCrLf & e.Exception.Message & _
vbCrLf & vbCrLf & e.Exception.StackTrace.ToString(), _
“Unexpected Error”, _ MessageBoxButtons.YesNo, _ MessageBoxIcon.Question) = DialogResult.Yes End Sub
Example program GlobalErrorHandler(available for download at www.vb-helper.com/one_on_ one.htm) uses the following code to directly call subroutine ProcessUnhandledExceptionto test that routine:
‘ Directly invoke the global error handler simulating a divide by zero
Private Sub btnInvokeErrorHandler_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles btnInvokeErrorHandler.Click
‘ Make a divide by zero exception
Try Dim i As Integer = 1 Dim j As Integer = 0
i = i \ j Catch ex As Exception
‘ Make the unhandled exception argument
Dim unhandled_args As New _ Microsoft.VisualBasic.ApplicationServices.UnhandledExceptionEventArgs( _ True, ex)
‘ Call ProcessUnhandledException directly
My.Application.ProcessUnhandledException(My.Application, unhandled_args)
‘ End the application if ExitApplication is True
If unhandled_args.ExitApplication Then End End Try
End Sub
The code uses a Try Catchblock that contains code that causes an error It makes an
UnhandledExceptionEventArgsobject for the resulting exception and passes it to the
ProcessUnhandledExceptionsubroutine
When UnhandledExceptionexecutes, the application ends automatically if the code sets
ExitApplicationto True This version of ProcessUnhandledExceptiondoes not automatically end the application, so the code here ends the program if ExitApplicationis True
Trang 7A second problem with this code is that the message displayed to the user is difficult to understand The
UnhandledExceptionevent handler doesn’t really know much about what is going on at the time of the error, so it can’t easily give the user meaningful information about what caused the problem The exception information does include a stack trace, so a developer might be able to guess what was hap-pening, but it would be difficult to tell the user how to fix the problem
It would be much better to catch the error closer to where it occurred There the program has a better chance of knowing what is happening and can give the user more constructive suggestions for fixing the problem
To move the error handling closer to the point of the actual error, make the UnhandledExceptionevent handler store the error information somewhere for developers to look at later It might note the error in a log file or email the information to developers Later, the developers can look at the saved stack trace, figure out what code caused the problem, and then add appropriate error-handling code so the error doesn’t make it all the way to UnhandledException
The program would then display a more generic message to the user simply explaining that an unex-pected error occurred, that the developers have been notified, and that the program is continuing (if it can) The program might also ask the user for information about what the program was doing when it encountered the error to give developers extra context
This solution helps address the problem of the error being caught far from the code that threw the excep-tion A subtler problem occurs when the code that throws the exception is far from the code that is responsible for causing the error For example, suppose one subroutine calculates a customer ID incor-rectly Much later, the program tries to access the customer and crashes when it finds that the customer doesn’t exist
In this case, the UnhandledExceptionevent handler will catch the error, display a message, and log information telling what code tried to access the customer Unfortunately, the real error occurred earlier when the program incorrectly calculated the customer ID By the time the problem becomes apparent, figuring out where the incorrect ID was calculated may be difficult
Sometimes, invalid values hide in the system for hours before causing trouble If invalid or corrupt data
is stored in a database, problems may arise months later when it’s too later to re-create the original data One solution to this problem of incorrect conditions causing problems later is to go hunting for bugs rather than letting them come to you
Hunting for Bugs
A basic problem with error handlers is that they only catch severe errors Visual Basic knows when the code tries to access an object that doesn’t exist, or tries to convert a SalesReportobject into an integer
By itself, Visual Basic has no way of knowing that a particular value should be a 7 instead of a 14, or that the program will later need access to a HotelReservationobject when the code sets it to Nothing Those are restrictions on the data imposed by the application rather than the Visual Basic language, so Visual Basic cannot catch them by itself Instead, you should add extra tests to the code to look for these sorts of invalid conditions and catch them as soon as they occur
Trang 8If you detect invalid conditions as soon as they arise, you can display meaningful error messages and log useful information about where the problem lies That makes fixing the problem much easier than it
is if you only have a record of where the problem makes itself obvious
Most of the trickiest bugs in C and C++ programming involve memory allocation In those languages, the program explicitly allocates and frees memory for objects The code can crash if you access an object that has no memory allocated for it, if you try to access an object that has been freed, or if you try to free the same memory more than once The real problem (for example, freeing an object that you will need
later) can occur long before you notice the problem (when you try to access the freed object) To make
matters worse, sometimes you can access the freed object and the program doesn’t crash.
These kinds of delayed memory problems are so difficult to debug that developers have written many
memory-management systems that add an extra layer to the memory-allocation system to watch for
these sorts of things Some go so far as to keep a record of every memory allocation and deallocation so that later, when you try to read memory that has been freed, you can figure out where it was freed.
Luckily for us, Visual Basic doesn’t use this sort of memory-allocation system, so this kind of bug can-not occur The problem of invalid conditions caused in one routine only appearing later in acan-nother rou-tine is still an issue, however, and can cause bugs that are very difficult to track down.
The “design by contract” (DBC) approach described in Chapter 3, “Agile Methodologies,” hunts for bugs proactively instead of waiting for them to occur and then responding In DBC, the program’s sub-systems explicitly verify pre- and post-conditions on the data Comments describe the state that the data must have before a routine is called, and the state the data will be in when the routine exits The code performs tests to verify those conditions and, if the one of the conditions fails, the code immediately stops so that you can fix the error
Normally, the code tests these conditions with Debug.Assertstatements If a condition is not satisfied and the program is running in the debugger, the program stops execution so that you can figure out what’s going wrong If the user is running the program in release mode, the Debug.Assertstatement is ignored and execution continues The idea is to thoroughly test the application in the debugger so that the assertions should not fail after you release the program to the end users
Sometimes, however, these sorts of bugs will slip through into the final application In that case, the
Debug.Assertstatements don’t help you because they are deactivated You can make them more pow-erful by performing the tests in your own Visual Basic code, rather than with Debug.Assertstatements Then you can perform the tests in the release version of the application in addition to the debug version The following code shows a ReleaseAssertsubroutine that you can use to perform assertions in the program’s release version The routine takes as parameters a Boolean condition to check and an optional message to display, much as Debug.Assertdoes
‘ If the assertion is False, display a message and a stack trace
Public Sub ReleaseAssert(ByVal assertion As Boolean, _
Optional ByVal message As String = “Assertion Failed”)
If Not assertion Then
‘ Get a stack trace
Dim stack_trace As New StackTrace(1, True)
Trang 9‘ Log the message appropriately My.Application.Log.WriteEntry( _ vbCrLf & Now & vbCrLf & _ message & vbCrLf & _ stack_trace.ToString() & _
“**********” & vbCrLf)
‘ Add a prompt and the stack trace
message &= vbCrLf & vbCrLf & _
“Do you want to try to continue running anyway?”
message &= vbCrLf & vbCrLf & stack_trace.ToString()
‘ Display the message
If MessageBox.Show( _ message, _
“Failed Assertion”, _ MessageBoxButtons.YesNo, _ MessageBoxIcon.Exclamation) = DialogResult.No _ Then
End End If End If End Sub
If the assertion fails, the subroutine makes a StackTraceobject representing the application’s current state The parameter 1makes the trace skip the topmost layer of the stack, which is the call to subroutine
ReleaseAssert The subroutine writes the current date and time, the message, and the stack trace into the program’s log file By default, this file is stored with a name having the following format:
base_path\company_name\product_name\product_version\app_name.log
Here the company_name, product_name, and product_versioncome from the assembly information
To view or change that information, open Solution Explorer, double-click My Project, and click the Assembly Information button
The base_nameis typically something similar to the following:
C:\Documents and Settings\user_name\Application Data
For example, the log file for the Assertionsexample program (which is available for download at
www.vb-helper.com/one_on_one.htm) is stored on my system at the following location:
C:\Documents and Settings\Rod.BENDER\
Application Data\TestCo\TestProd\1.2.3.4\Assertions.log
Subroutine ReleaseAssertthen displays a message to the user describing the problem and asking if the user wants to continue running anyway If the user clicks No, the program ends
Trang 10When one developer’s code calls routines written by another developer (or even a routine written by the same developer at a different time), there is a chance that the two developers had a different understand-ing of the data’s conditions, and that makes routine calls a productive place to put these sorts of DBC assertions However, those are not the only places where it makes sense to perform these kinds of tests Though the DBC philosophy only requires that you validate data during calls between routines, there’s
no reason you can’t make similar assertions at any point where you think a problem may creep into the data
Worthwhile places to inspect the data for correctness include the following:
❑ Places where data is created Was it created correctly?
❑ Places where data is transformed Was it transformed correctly?
❑ Places where data is moved from one place to another (between a database and the program, between an XML file and the program, between one subsystem and another) Was the data transferred correctly? Is there redundant data now left in the old location? Is there a way to ensure that they are synchronized?
❑ Places where data is discarded Are you sure you won’t need it later?
In one project, an algorithm developer was trying to decide where to put some error-checking code He was loading data from a database and needed to know that the data was clean When he asked whether
we should put error-checking code on the routines that saved the data into the database or on the algo-rithm code, the project manager and I simultaneously answered, “Both.” No matter how hard you try, errors eventually sneak into the data It may happen when new types of data slip past obsolete filters, or
it may be when a developer makes an incorrect change to validation code The only defense is to verify
all of the data as often as is practical.
In places such as these, it makes sense to use Debug.Assertto verify correctness If it won’t impact per-formance too much, it may also make sense to use a routine similar to ReleaseAssertto verify correct-ness in the program’s release version
Many developers resist extra data validation as inefficient They argue that the program doesn’t need to recheck the data, so you can leave these tests out to improve performance In most applications, however, performance is relative The application should be responsive enough for the users to get their jobs done, but most programs are much faster than necessary Is it really important to shave a few milliseconds off
of the application’s time so the idle process can use 99 percent of the CPU? In some applications, it may even make sense to have a background thread wandering through the data looking for trouble while
nothing else is happening.
I often add code to check inputs and outputs for every routine in an entire application Noted author
and expert developer John Mueller (www.mwt.net/~jmueller) does the same If you ask around, I
suspect you’ll find that a lot of top-notch developers check inputs and outputs practically to the point of paranoia It sounds like a lot of work, but it usually only takes a few minutes, and can easily catch bugs that might require hours or even days to find.
Don’t wait for bugs to find you through Try Catchblocks Use contract verification to validate data between routine calls Add extra condition checks whenever the data might become corrupted If the tests don’t take much time, use ReleaseAssertto keep them in the program’s release version The first time you discover a data problem near its source instead of hours and thousands of lines of code later, you’ll realize the extra work was worth the effort