Today's lesson covers a very important aspect of programming--handling runtime errors. Although you should always work to make sure your program can anticipate any problems that might occur while a user is running your software, you can't account for every possibility. That's why every good program should have a solid error-handling system.
Today you learn just what an error handler is and why error handlers are so important. You also learn about some of the inner workings of Visual Basic and how they affect error handling.
You learn about the difference between local error-handling methods and global error-handling methods. You also learn the advantages and disadvantages of each method. You see the various types of errors your program is likely to encounter and get some guidelines on how to handle each type of error.
You also learn about the Err object and the Error collection and how to use these objects to improve the accuracy of error reporting within your application. You also learn how to use the Raise method of the Err object to flag errors from within custom controls or OLE Server objects.
You also learn how to create error logs to keep track of errors that occur in your program. You learn how to create a trace log to analyze your programs. And you learn how you can write your programs to turn these features on or off without having to rewrite program code.
Finally, you build an OLE Server DLL that contains an improved error handler, an error logging facility, and a module trace routine. You can use this new OLE Server in all your future VBA-compliant programming projects.
Error handling is an essential part of any program. No program is complete unless it has good error handling. It is important to write your programs in a way that reduces the chance that errors occur, but you won't be able to think of everything. Errors do happen! Well-designed programs don't necessarily have fewer errors; they just handle them better.
Writing error handlers is not difficult. In fact, you can add consistent error handling to your program by adding only a few lines of code to each module. The difficult part of writing good error handlers is knowing what to expect and how to handle the unexpected. You learn how to do both in today's lesson.
Adding error handling to your program makes your program seem much more polished and friendly to your users. Nothing is more annoying--or frightening--to a user than seeing the screen freeze up, hearing a startling beep, or watching the program (and any file the user had been working on) suddenly disappear from the screen. This only needs to happen a few times before the user vows never to use your program again.
Polite error messages, recovery routines that allow users to fix their own mistakes or correct hardware problems, and opportunities for the user to save any open files before the program halts because of errors are all essential parts of a good error-handling strategy.
Writing error handlers in Visual Basic is a bit trickier than in most PC languages. There are several reasons for this. First, Visual Basic follows an event-driven language model, rather than a procedure-driven model like most PC languages. Second, Visual Basic uses a call stack method that isolates local variables. This means that when you exit the routine, you can lose track of the values of internal variables, which can make resuming execution after error handling difficult. Third, in Visual Basic all errors are local. If an error occurs, it's best to handle it within the routine in which the error occurred, which means you have to write a short error handler for each routine in your Visual Basic program.
NOTE: Technically, Visual Basic does allow the use of a global error handler. However, after Visual Basic travels up the procedure stack to locate the error handler, it can't travel back down the stack to resume execution after the error has been corrected. (This is typical of most object-oriented languages.) For this reason, we highly recommend using local error handlers in your Visual Basic programs.
Visual Basic 5 has two built-in objects that can be used to track and report errors at runtime. The Err object is a built-in object that exists in all Visual Basic programs. This object contains several properties and two methods. Each time an error occurs in the program, the Err object properties are filled with information you can use within your program.
The second built-in object that helps in tracking errors is the Error object, and its associated Errors collection. These are available to any Visual Basic 5 program that has loaded one of the Microsoft data access object libraries. The Error object is a child object of the DBEngine. You can use the Error object to get additional details on the nature of the database errors that occur in a program.
WARNING: The Error object is only available if you have loaded a Microsoft data-access object library. If you attempt to access the Error object from a Visual Basic program that does not have a Microsoft data access object library loaded, you receive an error.
The advantage of the Error object over the Err object is that the Error object contains more information about the database-related errors than the Err object. In some cases, back-end database servers return several error messages to your Visual Basic application. The Err object only reports the last error received from the back-end server. However, the Errors collection can report all of the errors received. For this reason, it is always a good idea to use the Error object when you are working with Visual Basic database applications.
Visual Basic 5 has a built-in object called the Err object. This object holds all the information about the most recent error that occurred within the running application space.
WARNING: There is a bit of confusion regarding the Err keyword in Visual Basic. Visual Basic 5 still supports the outdated Err and Error functions, but we do not advise you to use them in your programs. In some rare cases, the values reported by the Err and Error functions are not the same as those reported by the Err object. Throughout this book, when we mention "Err" we are referring to the Err object, not the Err function.
The Err object has several important properties. Table 14.1 shows these properties
and explains their use.
Table 14.1. The properties of the Err object.
Property | Type | Value |
Number | Long | The actual internal error number returned by Visual Basic. |
Source | String | Name of the current Visual Basic file in which the error occurred. This could be an EXE, DLL, or OCX file. |
Description | String | A string corresponding to the internal error number returned in the Number property, if this string exists. If the string doesn't exist, Description contains "Application-defined or object-defined error." |
HelpFile | String | The fully qualified drive, path, and filename of the Help file. This help file can be called to support the reported errors. |
HelpContext | Long | The Help file context (topic) ID in the help file indicated by the HelpFile property. |
LastDLLError | Long | The error code for the last call to a dynamic-link library (DLL). This is available only on 32-bit Microsoft platforms. |
For example, once an error occurs, you can inspect the properties of the object using the following code:
Msgbox "<" & CStr(Err.Number) & "> " & Err.Description & "[" & Err.Source & "]"
Once the error occurs and the Err object properties are updated, the Err object values do not change until another error is reported or the error-handling system is re-initialized.
NOTE: The error handling system is re-initialized each time a procedure exit or end occurs, or when the special error-handling keywords Resume or On Error are executed. You learn more about these keywords later in this chapter.
If the error that was reported has an associated help file and help context ID, these properties are also filled in. You can use the HelpFile and HelpContext properties of the Err object to display an online help topic to explain the error condition to the user.
TIP: Although help file composition and the construction of context-sensitive help is beyond the scope of a database book, Appendix B at the back of this book shows you how to create help files and how to link them to your Visual Basic programs.
If your application has called a dynamic-link library (DLL), you may be able to use the LastDLLError property of the Err object to get additional information about an error that occurred in a DLL. This property is only available on the 32-bit platform and may not be supported by the DLL you are calling.
In addition to the built-in Err object, Visual Basic 5 also has a built-in Error object for database errors. This object is a child object of the DBEngine object. For this reason, you can only access the Error object if you have loaded a Microsoft data access object library (select Project | References from the main menu).
The primary advantage of the Error object is that it can report additional error information not included in the standard Err object mentioned earlier in this chapter. Many times, your database application needs to depend on external processes such as ODBC data connections or OLE Server modules. When an error occurs in these external processes, they may report more than one error code back to your Visual Basic application.
The Err object is only able to remember the most recent error reported. However, the Error object (and its associated Errors collection) can remember all of the errors reported by external processes. That is why it is a good idea to use the Error object for reporting errors in all your Visual Basic database programs.
The properties of the Microsoft data access Error object are almost identical to the properties of the Visual Basic Err object (see the discussion earlier in this chapter). The only difference is that the Error object does not have the optional LastDLLError property. Therefore, the calling convention for the Error object is basically the same as that for the Err object:
Msgbox "<" & CStr(Error.Number) & "> " & Error.Description & "[" & Error.Source Â& "]"
While the Error and Err objects are quite similar, there is one major difference worth noting. While the Err object stands alone, the Microsoft data access Error object belongs to the Errors collection. This is very important when dealing with back-end database servers, especially when your Visual Basic program is connected to databases through the Open Database Connectivity (ODBC) interface. When an error occurs during an ODBC transaction, the Err object always returns the same error message, ODBC failed. However, the Errors collection often contains more than one error message, which can tell you a great deal more about the nature of the problem. You can retrieve all the error information by enumerating all the Error objects in the Errors collection. The code in Listing 14.1 shows how that can be done.
Dim objTempErr as Object Dim strMsg as String For Each objTempErr In Errors StrMsg = "<" & CStr(objTempErr.Number) & "> " StrMsg = strMsg & objTempErr.Description StrMsg = strMsg & " in [" & objTempErr.Source & "]" & vbCrLf Next Msgbox strMsg
The code in Listing 14.1 creates a single line of text (strMsg) that contains all
the error messages reported by the back-end database server. You learn more about
using both the Err and Error objects in the next section of the chapter.
Before getting into the details of using the Err and Error objects in your Visual Basic programs, let's take a look at a basic error handler in Visual Basic. Error handlers in Visual Basic have three main parts:
The On Error Goto statement appears at the beginning of the Sub or Function. This is the line that tells Visual Basic what to do when an error occurs, as in the following example:
On Error Goto LocalErrHandler
In the preceding code line, every time an error occurs in this Sub or Function, the program immediately jumps to the LocalErrHandler label in the routine and executes the error handler code. The error handler code can be as simple or as complex as needed to handle the error. A very simple error handler would just report the error number and error message, like this:
LocalErrHandler: MsgBox CStr(Err.Number) & " - " & Err.Description
In the preceding code example, as soon as the error occurs, Visual Basic reports the error number (Err.Number) and the error message (Err.Description) in a message box.
The third, and final, part of a Visual Basic error handler is the exit statement. This is the line that tells Visual Basic where to go after the error handler is done with its work. There are four different ways to exit an error handler routine:
Which exit method you use depends on the type of error that occurred and the error handling strategy you employ throughout your program. Error types and error handling strategies are covered later in this chapter.
Now that you have the basics of error handling, you can write some error-handling routines.
To start, let's write a simple error-handling routine to illustrate how Visual Basic behaves when errors occur. Start up a new Standard EXE project in Visual Basic 5. Add a single command button to the default form. Set its Name property to cmdSimpleErr and its Caption Property to Simple. Now add the code in Listing 14.2 to support the command button.
Private Sub cmdSimpleErr_Click() ` ` a simple error handler ` On Error GoTo LocalErr ` turn on error handling ` Dim intValue As Integer ` declare integer Dim strMsg As String ` declare string intValue = 10000000 ` create overflow error GoTo LocalExit ` exit if no error ` ` local error handler LocalErr: strMsg = CStr(Err.Number) & " - " & Err.Description ` make message MsgBox strMsg, vbCritical, "cmdSimpleErr_Click" ` show message Resume Next ` continue on ` ` routine exit LocalExit: ` End Sub
Save the form as BASICERR.FRM, and save the project as BASICERR.VBP.
Then execute the program and click the command button. You see the error message
displayed on the screen (see Figure 14.1).
The example in Listing 14.2 exhibits all the parts of a good error handler. The first line in the routine tells Visual Basic what to do in case of an error. Notice that the name for the error-handling code is given as LocalErr. (Every local error handler written in this book is called LocalErr.) Next, the routine declares an integer variable and then purposely loads that variable with an illegal value. This causes the error routine to kick in.
The error routine is very simple. It is a message that contains the error number
and the associated text message. The routine then displays that message along with
the warning symbol and the name of the routine that is reporting the error.
Figure
14.1. Displaying the results of a simple
error handler.
The next line tells Visual Basic what to do after the error is handled. In this case,
Visual Basic resumes execution with the line of program code that immediately follows
the line that caused the error (Resume at the Next line).
When Visual Basic resumes execution, the routine hits the line that tells Visual Basic to go to the exit routine (Goto LocalExit). Notice again the naming convention for the exit routine. All exit jump labels in this book are called LocalExit.
What happens if you get an error within your error routine? Although it isn't fun to think about, it can happen. When an error occurs inside the error-handling routine, Visual Basic looks for the next declared error routine. This would be an error routine started in the previous calling routine using the On Error Goto label statement. If no error routine is available, Visual Basic halts the program with a fatal error.
As an example, let's add a new button to the BASICERR project and create a cascading error condition. Set the button's Name property to cmdCascadeErr and its Caption property to Cascade. First, create a new Sub procedure called CreateErr. Then enter the code from List-ing 14.3.
Public Sub CreateErr() ` ` create an internal error ` On Error GoTo LocalErr ` Dim strMsg As String Dim intValue As Integer ` intValue = 900000 ` create an error GoTo LocalExit ` all done ` LocalErr: strMsg = CStr(Err.Number) & " - " & Err.Description MsgBox strMsg, vbCritical, "CreateErr" ` Open "junk.txt" For Input As 1 ` create another error Resume Next ` LocalExit: ` End Sub
Notice that this routine is quite similar to the code from Listing 14.2. The biggest
difference is in the lines of code in the error-handling portion of the subroutine.
Notice that Visual Basic attempts to open a text file for input. Since this file
does not currently exist, this action causes an error.
Now add the code from Listing 14.4 to the cmdCascadeErr_Click event. This is the code that calls the CreateErr routine.
Private Sub cmdCascadeErr_Click() ` ` create an error cascade ` On Error GoTo LocalErr ` Dim strMsg As String ` CreateErr ` call another routine GoTo LocalExit ` all done ` LocalErr: strMsg = CStr(Err.Number) & " - " & Err.Description MsgBox strMsg, vbCritical, "cmdCascadeErr" Resume Next ` LocalExit: ` End Sub
Save the program and run it to see the results. When you first click the command
button, you see the error message that announces the overflow error. Notice that
the title of the message box indicates that the error is being reported by the CreateErr
routine (see Figure 14.2).
Figure
14.2. Reporting the error from CreateErr.
When you click the OK button in the message box, you see another error message. This
one reports an Error 53-File not found message, which occurred when CreateErr
tried to open the nonexistent file (see Figure 14.3).
Figure
14.3. Reporting the File not
found error.
Here's the important point. Notice that the second error message box tells you that
the error is being reported from the cmdCascadeErr routine--even though
the error occurred in the CreateErr routine! The error that occurred in
the CreateErr error-handling routine could not be handled locally, and Visual
Basic searched upward in the call stack to find the next available error handler
to invoke. This action by Visual Basic can be a blessing and a curse. It's good to
know that Visual Basic uses the next available error-handling routine when things
like this happen, but it's also likely to cause confusion for you and your users
if you are not careful. For all you can tell in this example, an error occurred in
cmdCascadeErr. You must keep this in mind when you are debugging Visual
Basic error reports.
The simplest way to exit an error handler is to use the Resume method. When you exit an error handler with the Resume keyword, Visual Basic returns to the line of code that caused the error and attempts to run that line again. The Resume keyword is useful when you encounter an error that the user can easily correct, such as attempting to read a disk drive when the user forgot to insert a diskette or close the drive door. You can use the Resume keyword whenever you are confident that the situation that caused the error has been remedied, and you want to retry the action that caused the error.
Let's modify the BASICERR project by adding a new button to the project. Set its Name property to cmdResumeErr and its Caption property to Resume. Now add the Visual Basic code in Listing 14.5 to support the new button's Click event.
Private Sub cmdResumeErr_Click() ` ` show resume keyword ` On Error GoTo LocalErr ` Dim intValue As Integer Dim strMsg As String ` intValue = InputBox("Enter an integer:") GoTo LocalExit ` LocalErr: strMsg = CStr(Err.Number) & " - " & Err.Description MsgBox strMsg, vbCritical, "cmdResumeErr" Resume ` try it again ` LocalExit: ` End Sub
Save and run the project. When you press the Resume button, you are prompted to enter
an integer value. If you press the Cancel button or the OK button without entering
data (or if you enter a value that is greater than 32,767), you invoke the error
handler and receive an error message from Visual Basic (see Figure 14.4).
Figure
14.4. Reporting an error message from
the input box.
When you click the OK button, Visual Basic redisplays the input prompt and waits
for your reply. If you enter another invalid value, you see the error message, and
then you see the prompt again. This is the Resume exit method in action.
You can't get beyond this screen until you enter a valid value.
This can be very frustrating for users. What if they don't know what value to enter here? Are they stuck in this terrible error handler forever? Whenever you use the Resume keyword, you should give your users an option to ignore the error and move on or cancel the action completely. Those options are covered next.
Using the Resume Next method to exit an error handler allows your user to get past a problem spot in the program as if no error has occurred. This is useful when you use code within the error handler to fix the problem, or when you think the program can go on even though an error has been reported.
Deciding whether to continue the program even though an error has been reported is sometimes a tough call. It is usually not a good idea to assume that your program will work fine even though an error is reported. This is especially true if the error that occurs is one related to physical devices (missing diskette, lost communications connection, and so on) or file errors (missing, corrupted, or locked data files, and so on). The Resume Next keywords are usually used in error-handling routines that fix any reported error before continuing.
To illustrate the use of Resume Next, add a new command button to the project. Set its Name property to cmdResumeNext and its Caption property to Next. Now enter the code in Listing 14.6 behind the button's Click event.
Private Sub cmdResumeNextErr_Click() ` ` show use of resume next ` On Error GoTo LocalErr ` Dim intValue As Integer Dim strMsg As String Dim lngReturn As Long ` intValue = InputBox("Enter a valid Integer") MsgBox "intValue has been set to " + CStr(intValue) GoTo LocalExit ` LocalErr: If Err.Number = 6 Then ` was it an overflow error? strMsg = "You have entered an invalid integer value." & vbCrLf strMsg = strMsg & "The program will now set the value to 0 for you." & ÂvbCrLf strMsg = strMsg & "Select YES to set the value to 0 and continue." & ÂvbCrLf strMsg = strMsg & "Select NO to return to enter a new value." ` lngReturn = MsgBox(strMsg, vbCritical + vbYesNo, "cmdResumeNextErr") If lngReturn = vbYes Then intValue = 0 Resume Next Else Resume End If Else ` must have been some other error(!) strMsg = CStr(Err.Number) & " - " & Err.Description MsgBox strMsg, vbCritical, "cmdResumeNext" Resume End If ` LocalExit: ` End Sub
In Listing 14.6, you added a section of code to the error handler that tests for
the anticipated overflow error. You explain the options to the user and then give
the user a choice of how to proceed. This is a good general model for error handling
that involves user interaction. Tell the user the problem, explain the options, and
let the user decide how to go forward.
Notice, also, that this routine includes a general error trap for those cases when the error is not caused by an integer overflow. Even when you think you have covered all the possible error conditions, you should always include a general error trap.
Save and run this project. When you press the Next command button and enter an invalid value (that is, any number greater than 32,767), you see the error message that explains your options (see Figure 14.5).
There are times when you need your program to return to another spot within the
routine in order to fix an error that occurs. For example, if you ask the user to
enter two numbers that you use to perform a division operation, and it results in
a divide-by-zero error, you want to ask the user to enter both numbers again. You
might not be able to just use the Resume statement after you handle the
error.
Figure
14.5. An error message that asks for user
input.
When you need to force the program to return to a specific point in the routine,
you can use the Resume label exit method. The Resume label method
enables you to return to any place within the current procedure. You can't use Resume
label to jump to another Sub or Function within the project.
Now let's modify the BASICERR project to include an example of Resume label. Add a new command button to the project. Set its Name property to cmdResumeLabelErr and its Caption property to Resume Label. Now, place the code in Listing 14.7 behind the Click event.
Private Sub cmdResumeLabelErr_Click() ` ` show resume label version ` On Error GoTo LocalErr ` Dim intX As Integer Dim intY As Integer Dim intZ As Integer ` cmdLabelInput: intX = InputBox("Enter a Divisor:", "Input Box #1") intY = InputBox("Enter a Dividend:", "Input Box #2") intZ = intX / intY MsgBox "The Quotient is: " + Str(intZ), vbInformation, "Results" GoTo LocalExit ` LocalErr: If Err = 11 Then ` divide by zero error MsgBox CStr(Err.Number) & " - " & Err.Description, vbCritical, Â"cmdResumeLabelErr" Resume cmdLabelInput ` back for more Else MsgBox CStr(Err) & " -" & Error$, vbCritical, "cmdLabel" Resume Next End If ` LocalExit: ` End Sub
Save and run the project. Enter 13 at the first input box and 0
at the second input box. This causes a divide-by-zero error, and the error handler
takes over from there. You see the error message shown in Figure 14.6 and then return
to the line that starts the input process.
Figure
14.6. Displaying the Divide by Zero
error message.
There are times when an error occurs and there is no good way to return to the program. A good example of this type of error can occur when the program attempts to open files on a network file server and the user has forgotten to log onto the server. In this case, you need to either exit the routine and return to the calling procedure, or exit the program completely. Exiting to a calling routine can work if you have written your program to anticipate these critical errors. Usually it's difficult to do that. Most of the time, critical errors of this type mean you should end the program and let the user fix the problem before restarting the program.
Let's add one more button to the BASICERR project. Set its Caption property to End and its Name property to cmdEndErr. Enter the code in Listing 14.8 to support the cmdEnd_Click event.
Private Sub cmdEndErr_Click() ` ` use End to exit handler ` On Error GoTo LocalErr ` Dim strMsg As String Open "junk.txt" For Input As 1 GoTo cmdEndExit ` LocalErr: If Err.Number = 53 Then strMsg = "Unable to open JUNK.TXT" & vbCrLf strMsg = strMsg & "Exit the program and check your INI file" & vbCrLf strMsg = strMsg & "to make sure the JUNKFILE setting is correct." MsgBox strMsg, vbCritical, "cmdEnd" Unload Me Else MsgBox Str(Err) + " - " + Error$, vbCritical, "cmdEnd" Resume Next End If ` LocalExit: ` End Sub
In Listing 14.8, you add a check in the error handler for the anticipated File
not found error. You give the user some helpful information and then tell him
you are closing down the program. It's always a good idea to tell the user when you
are about to exit the program. Notice that you did not use the Visual Basic End
keyword; you used Unload Me. Remember that End stops all program
execution immediately. Using Unload Me causes Visual Basic to execute any
code placed in the Unload event of the form. This event should contain any
file-closing routine needed to safely exit the program.
Save and run the project. When you click the End button, you see a message box
explaining the problem and suggesting a solution (see Figure 14.7). When you click
the OK button, Visual Basic ends the program.
Figure
14.7. Showing the error message before
exiting the program.
Many times it is not practical, or desirable, to display an error message when an error occurs. Other times, you may want to use the error-handling capabilities of Visual Basic 5 to your advantage by creating your own error codes and messages. You can do this using the Raise method of the Err object. Using the Raise method allows you to alert users (or other calling applications) that an error has occurred, but gives both you and the user additional flexibility on how the error is handled.
The Raise method takes up to five parameters:
When you raise your own errors, you are required to report an error number. This number can be any unique value. If you use a number already defined as a Visual Basic error, you automatically get the ErrorDescription and any associated HelpFile and HelpContextID information as well. If you generate your own unique number, you can fill in the other parameters yourself. It is recommended that you use the vbObjectError constant as a base number for your own error codes. This guarantees that your error number does not conflict with any Visual Basic errors.
Here is a typical call to use the Err.Raise method:
` LocalErr: ` trouble with file stuff! Err.Raise vbObjectError + 1, "errHandler.LogError", "Can't write log file [" Â& errLogFileName & "]" ` End Sub
It is especially important to use this method for marking errors when you are coding ActiveX DLL servers. Since servers may run at a remote location on the network, you cannot be sure that users ever see any error dialog you display. Also, remember that, even if the DLL is running on the local PC, error dialog boxes are application-modal. No other processing occurs until the dialog box is dismissed. You learn to use the Err.Raise method when you create your errHandler object library later in this chapter.
So far, you have seen how to build a simple error handler and the different ways to exit error handlers. Now you need to learn about the different types of errors that you may encounter in your Visual Basic programs and how to plan for them in advance.
In order to make writing error handlers easier and more efficient, you can group errors into typical types. These error types can usually be handled in a similar manner. When you get an idea of the types of errors you may encounter, you can begin to write error handlers that take care of more than one error. You can write handlers that take care of error types.
There are four types of Visual Basic errors:
Each of these types of errors needs to be handled differently within your Visual Basic programs. You learn general rules for handling these errors in the following sections.
General file errors occur because of invalid data file information such as a bad filename, data path, or device name. Usually the user can fix these errors, and the program can continue from the point of failure. The basic approach to handling general file errors is to create an error handler that reports the problem to the user and asks for additional information to complete or retry the operation.
In Listing 14.9, the error handler is called when the program attempts to open a control file called CONTROL.TXT. The error handler then prompts the user for the proper file location and continues processing. Start a new Standard EXE project (ERRTYPES.VBP) and add a command button to the form. Set its Caption property to Control and its Name property to cmdControl. Also, add a CommonDialog control to the project. Enter the code in Listing 14.9 into the cmdControl_Click event.
Private Sub cmdControl_Click() ` ` show general file errors ` On Error GoTo LocalErr ` Dim strFile As String Dim strMsg As String Dim lngReturn As Long ` strFile = "\control.txt" ` Open strFile For Input As 1 MsgBox "Control File Opened" GoTo LocalExit ` LocalErr: If Err.Number = 53 Then ` file not found? strMsg = "Unable to Open CONTROL.TXT" & vbCrLf strMsg = strMsg & "Select OK to locate CONTROL.TXT" & vbCrLf strMsg = strMsg & "Select CANCEL to exit program." ` lngReturn = MsgBox(strMsg, vbCritical + vbOKCancel, "cmdControl") ` If lngReturn = vbOK Then CommonDialog1.filename = strFile CommonDialog1.DefaultExt = ".txt" CommonDialog1.ShowOpen Resume Else Unload Me End If Else MsgBox CStr(Err.Number) & " - " + Err.Description Resume Next End If ` LocalExit: ` End Sub
Save the form as FRMERRTYPES.FRM and the project as PRJERRTYPES.VBP.
Now run this project. When you click on the Control button, the program tries to
open the CONTROL.TXT file. If it can't be found, you see the error message
(see Figure 14.8).
Figure
14.8. Displaying the File not Found
error.
If the user selects OK, the program calls the CommonDialog control and prompts
the user to locate the CONTROL.TXT file. It can be found in the \TYSDBVB5\SOURCE\CHAP14
directory (see Figure 14.9).
Figure 14.9. Attempting to locate the CONTROL.TXT file.
TIP: Notice the use of the CommonDialog control to open the file. Whenever you need to prompt users for file-related action (open, create, save), you should use the CommonDialog control. This is a familiar dialog for your users, and it handles all of the dirty work of scrolling, searching, and so on.
Table 14.2 lists errors that are similar to the File not found error
illustrated in Listing 14.9. Errors of this type usually involve giving the user
a chance to re-enter the filename or reset some value. Most of the time, you can
write an error trap that anticipates these errors, prompts the user to supply the
corrected information, and then retries the operation that caused the error.
Table 14.2. Common general file errors.
Error Code | Error Message |
52 | Bad filename or number |
53 | File not found |
54 | Bad file mode |
55 | File already open |
58 | File already exists |
59 | Bad record length |
61 | Disk full |
62 | Input past end of file |
63 | Bad record number |
64 | Bad filename |
67 | Too many files |
74 | Can't rename with different drive |
75 | Path/File access error |
76 | Path not found |
Another group of common errors is caused by problems with physical media. Unresponsive printers, disk drives that do not contain diskettes, and downed communications ports are the most common examples of physical media errors. These errors might, or might not, be easily fixed by your user. Usually, you can report the error, wait for the user to fix the problem, and then continue with the process. For example, if the printer is jammed with paper, all you need to do is report the error to the user, and then wait for the OK to continue.
Let's add another button to the PRJERRTYPES.VBP project to display an example of physical media error handling. Add a new command button to the project. Set its Caption property to &Media and its Name property to cmdMedia. Enter the code in Listing 14.10 into the cmdMedia_Click event.
Private Sub cmdMedia_Click() ` ` show handling of media errors ` On Error GoTo LocalErr Dim strMsg As String Dim lngReturn As Long ` ` open a file on the a drive ` an error will occur if there ` is no diskette in the drive ` Open "a:\junk.txt" For Input As 1 Close #1 GoTo LocalExit ` LocalErr: If Err.Number = 71 Then strMsg = "The disk drive is not ready." & vbCrLf strMsg = strMsg + "Please make sure there is a diskette" & vbCrLf strMsg = strMsg + "in the drive and the drive door is closed." ` lngReturn = MsgBox(strMsg, vbCritical + vbRetryCancel, "cmdMedia") ` If lngReturn = vbRetry Then Resume Else Resume Next End If Else MsgBox Str(Err.Number) & " - " & Err.Description Resume Next End If ` LocalExit: ` End Sub
In Listing 14.10, you attempt to open a file on a disk drive that has no disk (or
has an open drive door). The error handler prompts the user to correct the problem
and allows the user to try the operation again. If all goes well the second time,
the program continues. The user also has an option to cancel the operation.
Save and run the project. When you click on the Media button, you should get results
that look like those in Figure 14.10.
Figure
14.10. The results of a physical media
error.
Another common type of error is the program code error. These errors occur as part of the Visual Basic code. Errors of this type cannot be fixed by users and are usually due to unanticipated conditions within the code itself. Error messages such as Variable Not Found, Invalid Object, and so on, create a mystery for most of your users. The best way to handle errors of this type is to tell the user to report the message to the programmer and close the program safely.
A very common type of error that occurs in database applications is the data-related error. These errors include those that deal with data type or field size problems, table access restrictions including read-only access, locked tables due to other users, and so on. Database errors fall into two groups. Those caused by attempting to read or write invalid data to or from tables, including data integrity errors, make up the most common group. The second group includes those errors caused by locked tables, restricted access, or multiuser conflicts.
NOTE: Errors concerning table locks, restricted access, and multiuser issues are covered in depth in the last week of the book. In this chapter, you focus on the more common group of errors.
In most cases, all you need to do is trap for the error, report it to the user, and allow the user to return to the data entry screen to fix the problem. If you use the Visual Basic data control in your data forms, you can take advantage of the automatic database error reporting built into the data control. As an example, let's put together a simple data entry form to illustrate some of the common data entry-oriented database errors.
Start a new Visual Basic Standard EXE project to illustrate common database errors.
Add a data control, two bound input controls, and two label controls. Use Table 14.3
as a reference for adding the controls to the form. Refer to Figure 14.11 as a guide
for placing the controls.
Figure
14.11. Laying out the DataErr form.
Table 14.3. Controls for the frmDataErr form.
Control | Property | Setting |
VB.Form | Name | frmDataErr |
Caption | "Data Error Demo" | |
ClientHeight | 1335 | |
ClientLeft | 60 | |
ClientTop | 345 | |
ClientWidth | 4665 | |
StartUpPosition | 3 `Windows Default | |
VB.CommandButton | Name | cmdAdd |
Caption | "&Add" | |
Height | 375 | |
Left | 3300 | |
Top | 60 | |
Width | 1215 | |
VB.TextBox | Name | txtName |
DataField | "Name" | |
DataSource | "Data1" | |
Height | 315 | |
Left | 1500 | |
Top | 540 | |
Width | 3015 | |
VB.TextBox | Name | txtKeyField |
DataField | "KeyField" | |
DataSource | "Data1" | |
Height | 375 | |
Left | 1500 | |
Top | 60 | |
Width | 1515 | |
VB.Data | Name | Data1 |
Align | 2 `Align Bottom | |
Caption | "Data1" | |
Connect | "Access" | |
DatabaseName | C:\TYSDBVB5\SOURCE\DATA\ERRORS\ | |
ERRORDB.MDB | ||
Height | 360 | |
RecordSource | "Table1" | |
Top | 975 | |
Width | 4665 | |
VB.Label | Name | lblName |
Caption | "Name" | |
Height | 255 | |
Left | 120 | |
Top | 540 | |
Width | 1215 | |
VB.Label | Name | lblKeyField |
Caption | "Key Field" | |
Height | 255 | |
Left | 120 | |
Top | 120 | |
Width | 1215 |
Private Sub cmdAdd_Click() Data1.Recordset.AddNew End Sub
Now save the new form as DATAERR.FRM and the project as DATAERR.VBP. When you run the project, you can test the built-in error trapping for Microsoft data controls by adding a new, duplicate record to the table. Press the Add button, then enter KF109 in the KeyField input box and press one of the arrows on the data control to force it to save the record. You should see a database error message that looks like the one in Figure 14.12.
Are you surprised? You didn't add an error trap to the data entry form, but you
still got a complete database error message! The Visual Basic data control is kind
enough to provide complete database error reporting even if you have no error handlers
in your Visual Basic program. Along with the automatic errors, the data control also
has the Error event. Each time a data-related error occurs, this event occurs.
You can add code in the Data1_Error event to automatically fix errors, display
better error messages, and so on.
Figure
14.12. A sample Microsoft data control
error message.
Let's modify the program a bit to show you how you can use the Data1_Error
event. First, add a CommonDialog control to your form. Then edit the DatabaseName
property of the data control to read C:\ERRORDB.MDB. Next, add the code
from Listing 14.11 to the Data1_Error event.
Private Sub Data1_Error(DataErr As Integer, Response As Integer) ` ` add error-trapping for data errors ` Dim strFileName As String ` Select Case DataErr Case 3044 ` database not found MsgBox "Unable to locate data file", vbExclamation, "Database ÂMissing" ` CommonDialog1.DialogTitle = "Locate ERRORDB.MDB" CommonDialog1.filename = "ERRORDB.MDB" CommonDialog1.Filter = "*.mdb" CommonDialog1.ShowOpen Data1.DatabaseName = CommonDialog1.filename ` Response = vbCancel ` cancel auto-message End Select ` End Sub
Notice that the code in Listing 14.11 checks to see whether the error code is 3044.
This is the error number that corresponds to the "database missing" message.
If the 3044 code is reported, the user sees a short message and then the
file open dialog, ready to locate and load the database. Finally, notice the line
that sets the Response parameter to vbCancel. This step tells Visual
Basic not to display the default message.
TIP: Usually, it is not a good idea to attempt to override this facility with your own database errors. As long as you use the Visual Basic data control, you do not need to add database error-trapping routines to your data entry forms. The only time you need to add error-related code is when you want to perform special actions in the Error event of the data control.
You need to add one more bit of code to complete this error trap. Add the following line of code to the Form_Activate event.
Private Sub Form_Activate() Data1.Refresh End Sub
This code makes sure the data entry fields on the form are updated with the most recent data from the database.
Now save and run the project. You first see a message telling you that the database
is missing (Figure 14.13).
Figure
14.13. Custom error message in the Data1_Error
event.
Next, the open file dialog waits for you to locate and load the requested database
(Figure 14.14).
Figure
14.14. Locating the requested database.
Finally, once you load the database, the data entry screen comes up ready for your
input.
If you use Microsoft data access objects instead of the Visual Basic data control, you need to add error-handling routines to your project. For example, if you want to create a Dynaset using Visual Basic code, you need to trap for any error that might occur along the way.
Add the code in Listing 14.12 to the Form_Load event of frmData. This code opens the database and creates a Dynaset to stuff into the data control that already exists on the form.
Private Sub Form_Load() ` ` create recordset using DAO ` On Error GoTo LocalErr ` Dim ws As Workspace Dim db As Database Dim rs As Recordset Dim strSQL As String ` strSQL = "SELECT * FROM Table2" Set ws = DBEngine.Workspaces(0) Set db = ws.OpenDatabase(App.Path & "\..\..\Data\Errors\ErrorDB.mdb") Set rs = db.OpenRecordset(strSQL, dbOpenDynaset) Exit Sub ` LocalErr: MsgBox "<" & CStr(Errors(0).Number) & "> " & Errors(0).Description, ÂvbCritical, "Form_Load Error" Unload Me ` End Sub
The code in Listing 14.12 establishes some variables and then opens the database
and creates a new Dynaset from a data table called Table2.
NOTE: Notice that instead of the Visual Basic Err object, the DAO Errors collection is used to retrieve the most recent database error. The Errors collection is only available if you loaded the Microsoft DAO library using the Project | References option from the main Visual Basic 5 menu.
Because there is no Table2 in ERRORDB.MDB, you see a database error when the program
runs. The error message is displayed, and then the form is unloaded completely (see
Figure 14.15).
Figure
14.15. Displaying an error message from
the Form_Load event.
It is a good idea to open any data tables or files that you need for a data entry
form during the Form_Load event. That way, if there are problems, you can
catch them before data entry begins.
In the previous sections, you created several error handlers, each tuned to handle a special set of problems. Although this approach works for small projects, it can be tedious and burdensome if you have to put together a large application. Also, after you've written an error handler that works well for one type of error, you can use that error handler in every other program that might have the same error. Why write it more than once?
Even though Visual Basic requires error traps to be set for each Sub or Function, you can still create a generic approach to error handling that takes advantage of code you have already written. In this section, you write a set of routines that you can install in all your Visual Basic programs--the error-handling OLE Server. This OLE Server offers some generic error-handling capabilities along with the ability to log these errors to a disk file and to keep track of the procedure call stack. These last two services can be very valuable when you encounter a vexing bug in your program and need to get additional information on the exact subroutines and functions that were executed before the error occurred.
To build the new error handling OLE Server, you need to start a new Visual Basic ActiveX DLL project. Name the default Class module errHandler and set the project Name to prjErrHandler. You also need to add a BAS module and a form to the project. The form acts as the new customized error dialog box. The BAS module holds a new user-defined type and some API definitions for use with the customized error dialog box.
Building the errHandler class involves several steps. First, add some code to the general declaration section of the Class object (see Listing 14.13).
Option Explicit ` ` error types Enum errType erritem = 0 errcoll = 1 End Enum ` ` return/option values Enum errReturn errExit = 0 errresume = 1 errNext = 2 errselect = 3 End Enum ` ` handler storage Private errDefRtn As errReturn Private errDefType As errType `
The first two items in the declaration section define enumerated types. These are
a special type of user-defined type that are a mix between a standard user-defined
type and a Public constant. Enumerated types make it easy to write well-documented
code. Along with the enumerated types, you see two Private variables declared for
local use.
Next, you need to add some declaration code to the BAS module in your DLL project. This code defines a special custom data type that you can use to control the display of your custom error dialog box. The errHandler DLL allows you to access any possible help topics associated with the error messages, too. For this reason, the BAS module contains the WinHelp API declaration. This is used on the custom dialog box. Open the BAS module, set its Name property to modErrHandler and enter the code in Listing 14.14 into the general declaration section of the module.
Option Explicit ` ` define dialog data type Public Type errDialog Message As String Buttons As Variant Title As String HelpFile As String HelpID As Long Return As Long End Type ` Public udtErrDialog As errDialog ` ` declare winHelp API Declare Function WinHelp Lib "user32" Alias "WinHelpA" (ByVal hwnd As Long, _ ByVal lpHelpFile As String, _ ByVal wCommand As Long, _ ByVal dwData As Long) As Long ` Public Const HELP_CONTEXT = &H1 Public Const HELP_QUIT = &H2
That's all that you need to add to the modErrHandler. Next, you need to
add code to the Initialize event of the errHandler Class module. Add the
code from Listing 14.15 to the Class_Initialize event.
Private Sub Class_Initialize() ` ` set starting values errDefRtn = errExit errDefType = errItem ` udtErrDialog.Buttons = "" udtErrDialog.HelpFile = "" udtErrDialog.HelpID = -1 udtErrDialog.Message = "" udtErrDialog.Return = -1 udtErrDialog.Title = "" ` End Sub
This errHandler class has two Public properties--the DefaultAction and DefaultType
properties. These defaults were set in the Initialize event and can be overridden
by setting the properties at runtime. Create the DefaultAction property (using the
Tools | Add Procedure menu option) and add the code from Listing 14.16 into the Property
Let and Property Get routines.
Public Property Get DefaultAction() As errReturn ` ` return default ` DefaultAction = errDefRtn ` End Property Public Property Let DefaultAction(ByVal vNewValue As errReturn) ` ` verify parm and store ` If vNewValue >= errExit Or vNewValue <= errselect Then errDefRtn = vNewValue End If ` End Property
Note that the data type for the property is errReturn. This is one of the enumerated
types defined in the declaration section of the class module. Next, create the Property
Let and Property Get routines for the DefaultType property, and enter
the code from Listing 14.17 into the project.
Public Property Get DefaultType() As errType ` DefaultType = errDefType ` End Property Public Property Let DefaultType(ByVal vNewValue As errType) ` If vNewValue >= errcoll Or vNewValue <= erritem Then errDefType = vNewValue End If ` End Property
Finally, you're ready to write the main error handler method. This method can be
called from any VBA-compliant program. You pass the Visual Basic Err object (or database
Errors collection) along with a few optional parameters that can control the behavior
of the message dialog box. After the method has been completed, a value is returned.
This value can be used to control program flow and error recovery.
Create a new function method in your class module called errHandler and enter the code from Listing 14.18.
Public Function errHandler(objErrColl As Variant, Optional intType As errType, ÂOptional errOption As errReturn, Optional errRefName As String) As errReturn ` -------------------------------------------------------------------------- ` produce msg and prompt for response ` ` inputs: ` objErrColl - DAO collection -OR- VBA Err object ` intType - errType enum that describes objErrColl (coll or item) ` errOption - errReturn enum sets dialog behavior (exit, resume, next, Âselect) ` errRefName - string to reference caller ` ` returns: ` errExit - end program ` errResume - try again ` errNext - skip to next line ` -------------------------------------------------------------------------- ` Dim strMsg As String Dim strTitle As String Dim rtnValue As errReturn ` ` retrieve action option If IsMissing(errOption) Then errOption = errDefRtn End If ` ` retrieve reference name If IsMissing(errRefName) Then errRefName = "" Else errRefName = " from " & errRefName End If ` ` build full message strMsg = errMsg(objErrColl, intType) ` ` write it out, if allowed If errLogFlag = True Then LogError strMsg End If ` ` evaluate things Select Case errOption Case errExit udtErrDialog.Title = "Exiting Program" udtErrDialog.Message = strMsg udtErrDialog.Buttons = Array("&Exit") frmErrDialog.Show vbModal rtnValue = errExit ` Case errresume, errNext udtErrDialog.Title = "Error Message" & errRefName udtErrDialog.Message = strMsg udtErrDialog.Buttons = Array("&OK") frmErrDialog.Show vbModal rtnValue = errOption ` Case Else udtErrDialog.Title = "Error Message" & errRefName udtErrDialog.Message = strMsg udtErrDialog.Buttons = Array("&Cancel", "&Retry", "&Ignore") frmErrDialog.Show vbModal rtnValue = udtErrDialog.Return End Select ` ` give it back errHandler = rtnValue ` End Function
The code in Listing 14.18 calls a support function (errMsg) and a dialog
box (frmErrDialog). You need to code these two remaining objects before you can test
your error handler. Create a new function (errMsg) in the class module and
enter the code from Listing 14.19 into the project.
Public Function errMsg(objErrColl As Variant, intType As errType) As String ` ` build and return complete error msg ` Dim strMsg As String Dim objItem As Error ` strMsg = "" ` If intType = errcoll Then For Each objItem In objErrColl strMsg = strMsg & "<" & CStr(objItem.Number) & "> " strMsg = strMsg & objItem.Description strMsg = strMsg & " (in " & objItem.Source & ")." & vbCrLf Next Else ` intType= errItem strMsg = "<" & objErrColl.Number & "> " strMsg = strMsg & objErrColl.Description strMsg = strMsg & " (in " & objErrColl.Source & ")" ` udtErrDialog.HelpFile = objErrColl.HelpFile udtErrDialog.HelpID = objErrColl.HelpContext End If ` errMsg = strMsg ` End Function
The main job of the errMsg routine is to build a complete error message
for display to the user. In order to do this, errMsg needs to know if the
object that was passed was a single VBA Err object or the DAO Errors collection.
That is why the errType parameter is included in the call. Also note that
the errMsg method has been declared as a Public method. You can
call this method from your Visual Basic 5 programs, too. That way, even if you don't
want to perform all of the error-handing operations, you can use errMsg
to get an improved error message for your use.
The last main piece of the errHandler class is a custom dialog box to display the error message and get input from the user. Often, you want to do more than just display the message and let the user press "OK." You may ask them to retry the same process, or ask if they want to ignore the error and continue. This dialog box not only displays the message and gives you an opportunity to get a response from the user, it also allows you to provide an optional "Help" button to give the user greater support in discovering how to resolve the error.
NOTE: Creating Help files for your Visual Basic project is covered in Appendix B of this book.
With the prjErrHandler project still open, add a form to the project. Use Table
14.4 and Figure 14.16 as guides in laying out the form.
Table 14.4. Control table for the frmErrDialog form.
Control | Property | Setting |
VB.Form | Name | frmErrDialog |
BorderStyle | 3 `Fixed Dialog | |
Caption | "Error Report" | |
ClientHeight | 1755 | |
ClientLeft | 45 | |
ClientTop | 330 | |
ClientWidth | 5460 | |
ControlBox | 0 `False | |
MaxButton | 0 `False | |
MinButton | 0 `False | |
ShowInTaskbar | 0 `False | |
StartUpPosition | 2 `CenterScreen | |
VB.TextBox | Name | txtErrMsg |
BackColor | &H80000000& | |
Height | 1035 | |
Left | 900 | |
Locked | -1 `True | |
MultiLine | -1 `True | |
ScrollBars | 2 `Vertical | |
Top | 120 | |
Width | 4395 | |
VB.CommandButton | Name | cmdBtn |
Caption | "&Help" | |
Height | 315 | |
Index | 3 | |
Left | 4080 | |
Top | 1320 | |
Visible | 0 `False | |
Width | 1215 | |
VB.CommandButton | Name | cmdBtn |
Caption | "&Ignore" | |
Height | 315 | |
Index | 2 | |
Left | 2760 | |
Top | 1320 | |
Visible | 0 `False | |
Width | 1215 | |
VB.CommandButton | Name | cmdBtn |
Caption | "&Retry" | |
Height | 315 | |
Index | 1 | |
Left | 1440 | |
Top | 1320 | |
Visible | 0 `False | |
Width | 1215 | |
VB.CommandButton | Name | cmdBtn |
Caption | "&OK" | |
Height | 315 | |
Index | 0 | |
Left | 120 | |
Top | 1320 | |
Visible | 0 `False | |
Width | 1215 | |
VB.Image | Name | Image1 |
Height | 600 | |
Left | 120 | |
Picture | C:\TYSDBVB5\SOURCE\CHAP14\ | |
ERRHANDLER\INTL_NO.BMP | ||
Stretch | -1 `True | |
Top | 120 | |
Width | 600 |
Private Sub Form_Load() ` Dim intBtns As Integer Dim intLoop As Integer ` txtErrMsg = udtErrDialog.Message Me.Caption = udtErrDialog.Title ` intBtns = UBound(udtErrDialog.Buttons) For intLoop = 0 To intBtns cmdBtn(intLoop).Caption = udtErrDialog.Buttons(intLoop) cmdBtn(intLoop).Visible = True cmdBtn(intLoop).Top = Me.ScaleHeight - 420 cmdBtn(intLoop).Left = 120 + (1300 * intLoop) Next ` ` check for help file If udtErrDialog.HelpFile <> "" Then cmdBtn(3).Visible = True cmdBtn(3).Top = Me.ScaleHeight - 420 cmdBtn(3).Left = 120 + (1300 * 3) End If ` End Sub
The code in Listing 14.20 first sets the dialog caption and message box. Then, based
on the properties of the udtErrDialog type, the buttons are arranged on the form.
The only other code that needs to be added to this form is the code that goes behind
the command button array. Place the code from Listing 14.21 in the cmdBtn_Click
event of the form.
Private Sub cmdBtn_Click(Index As Integer) ` ` return user selection ` Dim lngReturn As Long ` Select Case Index Case 0 udtErrDialog.Return = errExit lngReturn = WinHelp(Me.hwnd, udtErrDialog.HelpFile, HELP_QUIT, &H0) Unload Me Case 1 udtErrDialog.Return = errresume lngReturn = WinHelp(Me.hwnd, udtErrDialog.HelpFile, HELP_QUIT, &H0) Unload Me Case 2 udtErrDialog.Return = errNext lngReturn = WinHelp(Me.hwnd, udtErrDialog.HelpFile, HELP_QUIT, &H0) Unload Me Case 3 lngReturn = WinHelp(Me.hwnd, udtErrDialog.HelpFile, HELP_CONTEXT, udtErrDialog.HelpID) End Select ` End Sub
The code in Listing 14.21 allows the user to select the command button appropriate
for the moment. If the user presses the Help button, the properties from the udtErrDialog
type are used to fill in the parameters of the WinHelp API call.
That's all the code you need to create your errHandler ActiveX DLL. Save the project and then compile it (File | Make prjErrHandler.dll). If it compiles without error, you're all set for a quick test!
Start a new Visual Basic 5 Standard EXE project. Set the form name to frmTest
and the project name to prjTest. Add a data control and a command button to the form.
Refer to Figure 14.17 while laying out the form.
Figure
14.17. Laying out the frmTest form.
Next you need to add a reference to the error handler object library to your project.
Select Project | References and locate and add the prjErrHandler DLL (see Figure
14.18).
Figure
14.18. Adding the error handler object
library to the project.
Now you can add a bit of code to the form to set up the error handler, and then cause
an error to be handled. First, add the following line to the general declaration
section of the form. This declares the object that contains the error handler.
Option Explicit ` Public objErr As Object
Next, add the code from Listing 14.22 to the Form_Load event of the project.
Private Sub Form_Load() ` Data1.DatabaseName = "junk" Set objErr = New errHandler ` End Sub
This code creates the new error handler object and then sets up the data control
with a bogus database name. Now add the code from Listing 14.23 to the Data1_Error
event. This code intercepts the database error and displays the new custom dialog
box.
Private Sub Data1_Error(DataErr As Integer, Response As Integer) ` Dim rtn As Long Response = 0 rtn = objErr.errHandler(Errors, errcoll) ` End Sub
Now save the form (FRMTEST.FRM) and the project (PRJTEST.VBP) and
run the code. You should see your new object library error dialog telling you about
the database error (see Figure 14.19).
Now add some code behind the Command1_click event to create a divide-by-zero
error. The code in Listing 14.24 does just that.
Figure
14.19. The new error object library in
action.
Private Sub Command1_Click() ` On Error GoTo Localerr Dim rtn as Long ` Print 6 / 0 ` Exit Sub ` Localerr: rtn = objErr.errHandler(Err, erritem, errresume, "prjTest. ÂForm1.Command1_Click") Resume Next ` End Sub
When you save and run this code, press the command button to see the new error report.
You should see that the Help button is active. Press the Help button to display Visual
Basic 5 help for dealing with the divide-by-zero error (see Figure 14.20).
NOTE: Check out Appendix B for information on how you can create your own help files for your Visual Basic 5 projects.
Figure
14.20.Viewing help on the divide-by-zero error.
Now let's add an option that creates an error report file whenever the error handler
is activated.
When errors occur, users often do not remember details that appear in the error messages. It's much more useful to create an error log on disk whenever errors occur. This enables programmers or system administrators to review the logs and see the error messages without having to be right next to the user when the error occurs.
To build error-logging features into the existing errhandler class, you need to declare two new properties (LogFileName and WriteLogFlag), create a LogError method to write the errors to a disk file, add some code to the general declarations area, the Class_Initialize and Class_Terminate events, and add a few lines to the errHandler method to call the LogError method.
First, restart the errhandler DLL project and add the code below to the general declaration section of the class module.
` ` logging storage Private errLogFileName As String Private errLogFlag As Boolean
These two lines of code appear at the end of the section. They declare local storage space for the new properties. Now use the Tools | Add Procedure menu option to add two new Public properties to the class: LogFileName and WriteLogFlag. Listing 14.25 shows the code you need to add to the Property Let and Property Get statements for these two new properties.
Public Property Get LogFileName() As String ` LogFileName = errLogFileName ` End Property Public Property Let LogFileName(ByVal vNewValue As String) ` errLogFileName = vNewValue ` End Property Public Property Get WriteLogFlag() As Boolean ` WriteLogFlag = errLogFlag ` End Property Public Property Let WriteLogFlag(ByVal vNewValue As Boolean) ` errLogFlag = vNewValue `
End Property The LogFileName property is used to hold the name of the disk file that holds the log records. The LogFlag property controls the status of the error logging. If the LogFlag property is set to True, log records are created. Now add the following code to the end of the Class_Initialize event. This sets the default values for the two new properties.
errLogFileName = App.EXEName & ".err" errLogFlag = False `
Now create a new Private Sub method called LogError and enter the code from Listing 14.26 into the routine. This is the code that actually creates the log entries.
Private Sub LogError(strErrMsg As String) ` ` write error to disk file On Error GoTo LocalErr ` Dim intChFile As Integer ` intChFile = FreeFile Open errLogFileName For Append As intChFile Print #intChFile, Format(Now, "general date") Print #intChFile, strErrMsg Print #intChFile, "" Close intChFile ` Exit Sub ` LocalErr: ` trouble with file stuff! Err.Raise vbObjectError + 1, "errHandler.LogError", "Can't write log file [" & errLogFileName & "]" ` End Sub
Notice that you added an error handler in this routine. Because you are about to
perform disk operations, you need to be ready for errors here, too! Notice also that
the internal error is not displayed in a message box. Instead the Raise
method of the Err object is used to generate a unique error number and description.
This is sent back to the calling application for handling.
TIP: The Visual Basic FreeFile() function is used to return a number that represents the first available file channel that Visual Basic uses to open the data file. Using FreeFile() guarantees that you do not select a file channel that Visual Basic is already using for another file.
Now all you need to do is add a call to the LogError method from within the Public errHandler method. Listing 14.27 shows the code you need to add in the routine. Make sure you add these lines of code right after the call to the errMsg function and just before the start of the Select Case statement.
` build full message strMsg = errMsg(objErrColl, intType) ` ` write it out, if allowed <<< new code If errLogFlag = True Then <<< new code LogError strMsg <<< new code End If <<< new code ` ` evaluate things Select Case errOption
That's the end of the code to add logging to the error handler. Save the project
and compile the ActiveX DLL. Once the DLL has been successfully compiled, close this
project and open the test project you built earlier.
Open the Form_Load event of the frmTest form and add two lines to set the LogFileName and WriteLogFlag properties of the errHandler object. Listing 14.28 shows how to modify the code.
Private Sub Form_Load() ` Data1.DatabaseName = "junk" Set objErr = New errHandler ` objErr.WriteLogFlag = True objErr.LogFileName = App.Path & "\" & App.EXEName & ".log" ` End Sub
Now, when you run the project, each error is logged to a file with the same name
as the application in the same folder as the application. In the preceding example,
a file called errTest.log was created in the C:\TYSDBVB5\SOURCE\CHAP14\
folder. Listing 14.29 shows the contents of this error log file.
05-Feb-97 5:27:01 AM <3024> Couldn't find file `junk'. (in DAO.Workspace). 05-Feb-97 5:27:08 AM <11> Division by zero (in prjTest)
You can easily modify the layout and the contents of the log reports. You need only
change a few lines of code in the LogError method.
The final touch to add to your error handler library is the option to keep track of and print a module trace. A module trace keeps track of all the modules that have been called and the order in which they were invoked. This can be very valuable when you're debugging programs. Often, a routine works just fine when it is called from one module, but it reports errors if called from another module. When errors occur, it's handy to have a module trace to look through to help find the source of your problems.
We implement our module trace routines as a new objclass object in the prjErrHandler project. Reload the ActiveX DLL project and add a new class module to the project. Set its Name property to TraceObject and keep its Instancing property set to the default, 5 - MultiUse.
You need two new properties for this object (TraceFileName and TraceFlag) and a handful of new Public methods:
First, add the code in Listing 14.30 to the general declarations area of the class module.
Option Explicit ` ` local property storage Private trcFileName As String Private trcFlag As Boolean ` ` internal variables Private trcStack() As String Private trcPointer As Long
Next, create the two new Public properties, TraceFile and TraceFlag, and enter the
code from Listing 14.31 into the Property Let and Property Get
statements for these two new properties.
Public Property Get TraceFileName() As String ` TraceFileName = trcFileName ` End Property Public Property Let TraceFileName(ByVal vNewValue As String) ` trcFileName = vNewValue ` End Property Public Property Get TraceFlag() As Boolean ` TraceFlag = trcFlag ` End Property Public Property Let TraceFlag(ByVal vNewValue As Boolean) ` trcFlag = vNewValue ` End Property
Now add the code from Listing 14.32 to the Class_Initialize event. This
code sets the default values for the two Public properties.
Private Sub Class_Initialize() ` ` startup stuff trcFileName = App.EXEName & ".trc" trcFlag = False ` End Sub
Now it's time to code the various methods you need to manage call tracing in Visual
Basic 5. First, create the Public Sub methods Push and Pop.
These two routines handle the details of keeping track of each function or sub as
it is executed. Listing 14.33 shows the code for these two Public methods.
Public Sub Push(ProcName As String) ` ` push a proc onto the stack trcPointer = trcPointer + 1 ReDim Preserve trcStack(trcPointer) trcStack(trcPointer) = ProcName ` End Sub Public Sub Pop() ` ` pop a proc off the stack If trcPointer <> 0 Then trcPointer = trcPointer - 1 ReDim Preserve trcStack(trcPointer) End If ` End Sub
Now create another Public Sub method (Clear) and a Public Function
method (List). Add the code from Listing 14.34 to the class.
Public Function List() As Variant ` ` return an array of the trace log List = trcStack ` End Function Public Sub Clear() ` ` clear off the stack trcPointer = 0 ReDim Preserve trcStack(0) ` End Sub
TIP: Notice the use of the Variant data type to return an array of items. This is a very efficient way to pass array data among Visual Basic methods.
Now create a new Public Sub called Dump. This writes the trace list to a disk file. Fill in the method with the code from Listing 14.35.
Public Sub Dump() ` ` write trace log to file Dim intFile As Integer Dim intLoop As Integer ` intFile = FreeFile Open trcFileName For Append As intFile Print #intFile, "***TRACE STACK DUMP***" Print #intFile, "***DATE: " & Format(Now(), "general date") Print #intFile, "" ` For intLoop = trcPointer To 1 Step -1 Print #intFile, vbTab & Format(intLoop, "000") & ": " & ÂtrcStack(intLoop) Next ` Print #intFile, "" Print #intFile, "***EOF" Close #intFile ` Exit Sub ` LocalErr: Err.Raise vbObjectError + 3, "Trace.Dump", "Can't write trace file [" & ÂtrcFileName & "]" ` End Sub
Finally, create the Public Sub method called Show and enter the
code from Listing 14.36.
Public Sub Show() ` ` show trace log in dialog ` Dim intLoop As Integer Dim strMsg As String ` strMsg = "" For intLoop = trcPointer To 1 Step -1 strMsg = strMsg & Format(intLoop, "000") strMsg = strMsg & ": " strMsg = strMsg & Trim(trcStack(intLoop)) strMsg = strMsg & vbCrLf Next ` MsgBox strMsg, vbInformation, "Trace Stack" ` End Sub
Notice that the code in Listing 14.36 prints the call array in reverse order. This
is the conventional way to print trace lists. The top-most entry shows the most recently
executed routine, and the bottom-most entry shows the first routine in this trace.
After adding this last code, save and compile the ActiveX DLL and then load your errTest project. After you load the frmTest form, add the following code to the general declarations area of the form.
Public objTrace As Object
Next, update the Form_Load event as shown in Listing 14.37. This adds the use of the trace module to the project.
Private Sub Form_Load() ` Data1.DatabaseName = "junk" ` Set objErr = New errHandler Set objTrace = New TraceObject ` objTrace.Push "Form_Load" ` objErr.WriteLogFlag = True objErr.LogFileName = App.Path & "\" & App.EXEName & ".log" ` objTrace.Pop ` End Sub
Note the use of objTrace.Push to add the name of the method onto the trace
stack. This should happen as early as possible in the method code. Note, also the
objTrace.Pop line at the very end of the method. This removes the name of
the method from the stack just as the method is complete.
Let's also add trace coding to the Command1_click event. Update your form's command1_Click event to match the one in Listing 14.38.
Private Sub Command1_Click() ` On Error GoTo Localerr Dim varList As Variant Dim rtn As Long ` objTrace.Push "Command1_Click" ` Print 6 / 0 ` Exit Sub ` Localerr: ` rtn = objErr.errHandler(Err, erritem, errresume, "prjTest.Form1.Command1_Click") ` objTrace.Show objTrace.Pop Resume Next ` End Sub
Save this code and run the project. When you press the command button, you get a
trace report on the screen (see Figure 14.21).
Figure
14.21. Viewing the trace message.
Notice that, in order to add module tracing to a project, you only need to add a
.Push line at the start of the routine and a .Pop line at the end
of the routine. This is all you need to do in order to update the procedure stack
for the program. But, for this to be really valuable, you have to do this for each
routine that you want to track.
In a real application environment, you wouldn't want to show the procedure stack each time an error is reported. The best place for a stack dump is at exit time due to a fatal error. You should probably use the TraceFile option to write the stack to disk rather than displaying it to the user.
Now that you have the basics of error handling under your belt, you can continue to add features to the generic error handler. As you add these features, your programs take on a more professional look and feel. Also, using options such as error report logs and procedure stack logs makes it easier to debug and maintain your applications.
Additional features that you can add to your error handler include:
Today's lesson covered all the basics of creating your own error-handling routines for Visual Basic applications. You learned that an error handler has three basic parts:
You learned that an error handler has four possible exits:
You learned how to use the Err.Raise method to flag errors without resorting to modal dialog boxes.
You learned about the major types of errors that you are likely to encounter in your program:
You also learned that you can declare a global error handler or a local error handler. The advantage of the global error handler is that it allows you to create a single module that handles all expected errors. The disadvantage is that, due to the way Visual Basic keeps track of running routines, you are not able to resume processing at the point the error occurs once you arrive at the global error handler. The advantage of the local error handler is that you are always able to use Resume, Resume Next, or Resume label to continue processing at the point the error occurs. The disadvantage of the local error handler is that you need to add error-handling code to every routine in your program.
Finally, you learned how to create an error handler object library that combines local error trapping with global error messages and responses. The error handler object library also contains modules to keep track of the procedures currently running at the time of the error, a process for printing procedure stack dumps to the screen and to a file, and a process that creates an error log on file for later review.