Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 18 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
18
Dung lượng
419,24 KB
Nội dung
Bug 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. 22_053416 ch16.qxd 1/2/07 6:35 PM Page 435 Modern 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 Catch block. 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 Catch block or On Error statement, 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 Catch blocks 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 Catch block 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 Catch block and saves the program from crashing. 436 Part III: Development 22_053416 ch16.qxd 1/2/07 6:35 PM Page 436 Example program CrashProof uses the following code to protect three event handlers from crashing. The code in each event handler is contained in a Try Catch block. 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 CrashProof program (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 Catch blocks are missing. All of this leads to a common development strategy: ❑ Start by writing code with little or no error handling. ❑ Add Try Catch blocks 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 437 Chapter 16: Bug Proofing 22_053416 ch16.qxd 1/2/07 6:35 PM Page 437 invalid 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 Catch blocks to every event handler and Sub Main that 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 Catch block 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. 438 Part III: Development 22_053416 ch16.qxd 1/2/07 6:35 PM Page 438 This 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 Catch blocks elsewhere in your code. The following code shows the application module containing an UnhandledException event 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.ExitApplication to False so 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 Catch blocks. Note that the event handler must itself be crash-proof. If the UnhandledException event handler throws an exception, the program crashes. Use Try Catch blocks 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 UnhandledException error handler, setting e.ExitApplication to False, and then throwing the same error again. A second problem is that the UnhandledException event 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 UnhandledException event 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 UnhandledException event handler simply calls subroutine ProcessUnhandledException, which does all the work. Both of these routines are contained in the MyApplication class inside the ApplicationEvents.vb module where you would normally create the UnhandledException event handler. 439 Chapter 16: Bug Proofing 22_053416 ch16.qxd 1/2/07 6:35 PM Page 439 Private 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 ProcessUnhandledException to 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 Catch block that contains code that causes an error. It makes an UnhandledExceptionEventArgs object for the resulting exception and passes it to the ProcessUnhandledException subroutine. When UnhandledException executes, the application ends automatically if the code sets ExitApplication to True. This version of ProcessUnhandledException does not automatically end the application, so the code here ends the program if ExitApplication is True. 440 Part III: Development 22_053416 ch16.qxd 1/2/07 6:35 PM Page 440 A second problem with this code is that the message displayed to the user is difficult to understand. The UnhandledException event 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 UnhandledException event 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 UnhandledException event 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 SalesReport object 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 HotelReservation object 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. 441 Chapter 16: Bug Proofing 22_053416 ch16.qxd 1/2/07 6:35 PM Page 441 If 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 another 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.Assert statements. 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.Assert statement 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.Assert statements 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.Assert statements. Then you can perform the tests in the release version of the application in addition to the debug version. The following code shows a ReleaseAssert subroutine 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.Assert does. ‘ 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) 442 Part III: Development 22_053416 ch16.qxd 1/2/07 6:35 PM Page 442 ‘ 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 StackTrace object representing the application’s current state. The parameter 1 makes 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_version come 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_name is typically something similar to the following: C:\Documents and Settings\user_name\Application Data For example, the log file for the Assertions example 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 ReleaseAssert then 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. 443 Chapter 16: Bug Proofing 22_053416 ch16.qxd 1/2/07 6:35 PM Page 443 When 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.Assert to verify correctness. If it won’t impact per- formance too much, it may also make sense to use a routine similar to ReleaseAssert to 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 Catch blocks. 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 ReleaseAssert to 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. 444 Part III: Development 22_053416 ch16.qxd 1/2/07 6:35 PM Page 444 [...]...22_053 416 ch16.qxd 1/2/07 6:35 PM Page 445 Chapter 16: Bug Proofing Tr y Catch Blocks Try Catch blocks are the basic method for catching errors in Visual Basic NET Unlike the On Error statements used in Visual Basic 6, they are nice, block-oriented structures that make it easy to tell when the code is protected in an error handler and when an error has occurred Unfortunately,... it from happening again This chapter discusses ways you can catch and handle errors It tells where you should put error-handling code, how you can catch otherwise unhandled errors, and what you might want to do when those unhandled errors occur Chapter 17, “Testing,” explains advanced debugging and testing techniques It explains how you can devise tests to uncover bugs and how to track down bugs while... identify new errors and add appropriate error-handling code When you can find no other bugs, make an UnhandledException event handler to catch any unexpected errors that arise Make that event handler call another routine that does all the work so that you can test your error handling Be sure the global error-handling code is robust If this code fails, you’ll learn nothing about the error and won’t be able... My.Computer.FileSystem.FileExists and skip reading the file if the file doesn’t exist 446 22_053 416 ch16.qxd 1/2/07 6:35 PM Page 447 Chapter 16: Bug Proofing Figure 1 6-2 : Program ExceptionSpeeds shows it’s faster to check for dangerous conditions than to let Try Catch handle the resulting errors In one set of tests, program ExceptionSpeeds (available for download at www.vb-helper.com/ one_on_one.htm) took... UnhandledException event handler to deal with errors It tries to save the most detailed information for developers in the most convenient way possible If one method fails, it tries a simpler method that is more likely to succeed 448 22_053 416 ch16.qxd 1/2/07 6:35 PM Page 449 Chapter 16: Bug Proofing Private Sub MyApplication_UnhandledException(ByVal sender As Object, _ ByVal e As Microsoft.VisualBasic.ApplicationServices... clean design and well-defined architecture so that all developers have a good understand of what the different pieces of code should do Use agile techniques such as pair programming, code reviews, DBC, and test-driven development to reduce the number of bugs that slip into the code in the first place When you know that the code may contain bad data, look for it rather than letting an error handler... Exception End Try Try Step3 Catch ex As Exception End Try One solution to this problem is to mix the old and new styles of error handling Inside the three subroutines, you can use the newer Try Catch block while the calling routine uses On Error Resume Next Visual Basic will not let you mix the two error-handling styles in the same routine Another approach is to use a Do loop to repeatedly execute the steps... to look for possible problems and avoid throwing an exception than it is to rely on exception handling to validate the program’s data Responding to Bugs Suppose you apply all of the bug-proofing techniques at your disposal: good design, pair programming, code reviews, offensive programming, verified DBC, and thorough testing Now, when the program detects an error at run-time, what should it do? In part,... My.Computer.FileSystem.FileExists to see whether the file exists before you try to read the file Example program ExceptionSpeeds shown in Figure 1 6-2 demonstrates these errors Enter a number of trials and click the Divide By Zero button to make the program use a Try Catch block to catch a divide-by-zero error Click the Check For Zero button to make the program check whether the denominator is zero before performing the... email gets lost Finally, the code tells the user whether it sent email or made a log entry, and asks whether it should exit the application If neither the email nor the log entry worked, the program asks the user to call the developers to report the problem 450 22_053 416 ch16.qxd 1/2/07 6:35 PM Page 451 Chapter 16: Bug Proofing Instead of asking the user to call Technical Support, you could ask the user . 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. event handler. 439 Chapter 16: Bug Proofing 22_053 416 ch16.qxd 1/2/07 6:35 PM Page 439 Private Sub MyApplication_UnhandledException(ByVal sender As Object, _ ByVal e As Microsoft.VisualBasic.ApplicationServices catch and handle errors. It tells where you should put error-han- dling code, how you can catch otherwise unhandled errors, and what you might want to do when those unhandled errors occur. Chapter