Chapter 16. Error Handling: Preparing for the Inevitable

<feature><title>In This Chapter</title> </feature>

Why This Chapter Is Important

Errors happen, even in the absence of programmer error. You need to protect your programs and your data from the adverse effects of errors by practicing error handling.

Error handling (also known as error trapping) is the process of intercepting Jet’s or VBA’s response to an error. It enables the developer to determine the severity of an error and to take the appropriate action in response to the error. This chapter shows you the techniques required to successfully implement error handling within your applications.

Implementing Error Handling

Without error handling, the user of your application is forced to exit abruptly from your application code. Consider the example in Listing 16.1.

Example 16.1. An Example of Code Without Error Handling

Private Sub cmdNoErrorHandler_Click()
    'Call TestError1, passing the values in the txtValue1
    'and txtValue2 text boxes
    Call TestError1(Me.txtValue1.Value, Me.txtValue2.Value)
End Sub


Sub TestError1(Numerator As Integer, Denominator As Integer)
    'Divide the value received as the first parameter
    'by the value received as the second parameter
    Debug.Print Numerator / Denominator
    'If successful, display a message to the user
    MsgBox "I am in Test Error"
End Sub

The click event behind the command button calls the routine TestError1, passing it the values from two text boxes. TestError1 accepts those parameters and attempts to divide the first parameter by the second parameter. If the second parameter is equal to 0, a runtime error occurs. Because no error handling is in effect, the program terminates.

Figure 16.1 shows the error message the user receives. As you can see, the choices are Continue, End, Debug, and Help. If users click Debug, the module window appears, and they are placed in Debug mode on the line of code causing the error. Clicking Continue (this is not always available) tells Access to ignore the error and continue with the execution of the program. End terminates execution of the programming code. If the application is running with the runtime version of Access, it shuts down, and users are returned to Windows.

The default error handling message.

Figure 16.1. The default error handling message.

With error handling in effect, you can attempt to handle the error in a more appropriate way whenever possible.

You can add error-handling code to the error event procedure of a form or report. You also can add it to any VBA subroutine, function, or event routine. You easily can modify the code in Listing 16.1 to handle the error gracefully. The code in Listing 16.2 shows a simple error-handling routine.

Example 16.2. A Simple Error-Handling Routine

Sub TestError2(Numerator As Integer, Denominator As Integer)
On Error GoTo TestError2_Err
    'Divide the value received as the first parameter
    'by the value received as the second parameter
    Debug.Print Numerator / Denominator
    'If successful, display a message to the user
    MsgBox "I am in Test Error"

    Exit Sub

TestError2_Err:
    'If a divide by zero (error 11) occurs, display an
    'appropriate message to the user
    If Err = 11 Then
        MsgBox "Variable 2 Cannot Be a Zero", , "Custom Error Handler"
    End If
    Exit Sub

End Sub

This code is located in the basError module, which is in the CHAP16EX.MDB database on the accompanying CD-ROM.

A Simple Error-Handling Routine

The routine now invokes error handling. If a divide-by-zero error occurs, a message box alerts the user to the problem, as Figure 16.2 shows.

A custom error handler message.

Figure 16.2. A custom error handler message.

Using On Error Statements

On Error statements activate error handling. Each routine must contain its own On Error statement if you want that routine to do its own error handling. Otherwise, error handling is cascaded up the call stack. If no On Error statements are found in the call stack, VBA’s own error handling is invoked.

Suppose that Func1 calls Func2, and Func2 calls Func3. Only Func1 contains error handling. An error occurs in Func3. Func3 passes control up to Func2. Func2 has no error handling, so it passes control up to Func1. Func1 handles the error. Needless to say, the error handler found in Func1 is not necessarily appropriate to handle the error that occurred in Func3.

Using an On Error statement, you can cause the application to branch to error-handling code, resume code execution on the line immediately following the error, or attempt to re-execute the problem line of code.

You must decide the most appropriate response to a particular error. Sometimes it is most appropriate for your application to halt in response to an error. At other times, it is best if the routine skips the offending line entirely. By combining the use of On Error Goto, On Error Resume Next, and the Resume statement, you can handle each error appropriately.

Using On Error Goto

The statement On Error Goto <label> tells VBA that, from this point forward in the subroutine or function, if an error occurs, it should jump to the label specified in the statement. This is the most common form of error handling.

The label specified in the On Error statement must be located in the current procedure. Listing 16.3 shows a simple example of error handling.

Example 16.3. An Example of Error Handling Using the On Error GoTo Statement

Sub SimpleErrorHandler(iVar1 As Integer, iVar2 As Integer)
    'Invoke error handling
    On Error GoTo SimpleErrorHandler_Err

    'Declare a variable to hold the result
    Dim sngResult As Single
    'Divide the first parameter received by the
    'second parameter received
    sngResult = iVar1 / iVar2

    'Exit the subroutine if all went as planned
    Exit Sub

SimpleErrorHandler_Err:
    'If an error occurs, display a message and exit
    'the subroutine
    MsgBox "Oops!"
    Exit Sub

End Sub

You can learn some important things from this simple routine. The routine receives two integer values. It then invokes the error handler. When an error occurs, execution continues at the label. Notice that this routine contains two Exit Sub statements. If you remove the first Exit Sub statement, the code falls through to the label regardless of whether an error occurred. The Exit Sub statement at the bottom gracefully exits the procedure, setting the error code back to 0.

Including the Error Number and Description in the Error Handler

The error-handling code in Listing 16.3 did not give a very descriptive message to users. The Description and Number properties of the Err object give users more meaningful error messages. The Err object is covered in detail later in this chapter in the section “Using the Err Object.” For now, take a look at the Description and Number properties to see how you can use them to enhance an error-handling routine. To display the error number and description, you must modify the error-handling code to look like this:

SimpleErrorHandler_Err:
    'If an error occurs, display a message and exit
    'the subroutine
    MsgBox "Error #" & Err.Number & ": " & Err.Description
    Exit Sub

This time, instead of hard-coding the error message, you display the error number and VBA’s internal error string. Figure 16.3 shows the resulting error message. The SimpleErrorHandler routine and all the following examples are located in the basError module of the CHAP16EX.MDB database.

An error message with an error number and error string.

Figure 16.3. An error message with an error number and error string.

Using On Error Goto 0

You use On Error Goto 0 for two purposes:

  • When you want Access to return to its default error handler

  • When you have other error handling in a routine, but want Access to return to the calling routine when a specific condition occurs

Generally, you don’t want Access to return to its default error handler. You might do this only if you are unable to handle the error or if you are in the testing phase and not yet ready to implement your own error handler.

The reason why you want Access to return the error to a higher-level routine is much clearer. You do this if you want to centralize the error handling, meaning that one routine might call several others. Instead of placing error-handling code in each routine that is called, it is appropriate in certain situations to place the error handling in the calling routine.

Using On Error Resume Next

On Error Resume Next continues program execution on the line immediately following the error. This construct generally is used when it is acceptable to ignore an error and continue code execution. Listing 16.4 shows an example of such a situation.

Example 16.4. Ignoring an Error and Continuing Execution

Sub TestResumeNext()
    'Instruct VBA to continue on the next line if an error
    'occurs
    On Error Resume Next

    'Attempt to delete a file
    Kill "AnyFile"
    'If no error occurred, do nothing. Otherwise, display
    'a message with the description of the error that occurred
    If Err.Number = 0 Then
    Else
        MsgBox "We Didn't Die, But the Error Was: " & Err.Description
    End If
End Sub

You use the Kill statement to delete a file from disk. If the specified file is not found, an error results. You delete the file only if it exists, so you are not concerned about an error. On Error Resume Next is very appropriate in this situation because resuming execution after the offending line of code does no harm. The example illustrates that, although code execution proceeds, the properties of the error object are still set.

Using Resume Statements

While you are in your error-handling code, you can use the Resume, Resume Next, and Resume <LineLabel> statements to specify how you want VBA to respond to the error. Resume attempts to re-execute the offending line of code, Resume Next resumes execution after the offending line of code, and Resume <LineLabel> continues execution at a specified line label. The following sections cover these statements in detail.

The Resume Statement

The Resume statement resumes code execution on the line of code that caused the error. You must use this statement with extreme care because it can throw the code into an unrecoverable endless loop. Listing 16.5 shows an example of an inappropriate use of the Resume statement.

Example 16.5. Using Resume Inappropriately

Function BadResume(strFileName As String)
    'Invoke error handling
    On Error GoTo BadResume_Err
    Dim strFile As String

    'Perform the Dir function to determine if
    'the file passed as a parameter exists
    strFile = Dir(strFileName)

    'If the file doesn't exist, return False
    'Otherwise, return True
    If strFile = "" Then
      BadResume = False
    Else
      BadResume = True
    End If

    'Exit the function if all goes well
    Exit Function

BadResume_Err:
    'Display an error message with the
    'description of the error that occurred
    MsgBox Error.Description

    'Attempt to re-execute the offending line of code
    Resume
End Function

This function is passed a filename. The Dir function searches for a file with that name and returns True or False, depending on whether the specified file is found. The problem occurs when the drive requested is not available or does not exist. This code throws the computer into an endless loop. To remedy the problem, you should modify your code to look like the code in Listing 16.6.

Example 16.6. Using Resume Conditionally Based on User Feedback

Function GoodResume(strFileName As String)
    'Invoke error handling
    On Error GoTo GoodResume_Err
    Dim strFile As String

    'Perform the Dir function to determine if
    'the file passed as a parameter exists
    strFile = Dir(strFileName)

    'If the file doesn't exist, return False
    'Otherwise, return True
    If strFile = "" Then
      GoodResume = False
    Else
      GoodResume = True
    End If

    'Exit the function if all goes well
    Exit Function
GoodResume_Err:
    Dim intAnswer As Integer

    'Ask user if they want to try again
    intAnswer = MsgBox(Error & ", Would You Like to Try Again?", vbYesNo)

    'If they respond yes, attempt to re-execute the offending line
    'of code. Otherwise, exit the function
    If intAnswer = vbYes Then
        Resume
    Else
        Exit Function
    End If
End Function

In this example, the error handler enables the user to decide whether to try again. The Resume occurs only if the user’s response is affirmative.

The Resume Next Statement

Just as you can invoke error handling using an On Error Resume Next statement, you can place a Resume Next statement in your error handler, as Listing 16.7 shows.

Example 16.7. Placing a Resume Next Statement in Your Error Handler

Sub TestResumeNextInError()

    'Invoke error handling
    On Error GoTo TestResumeNextInError_Err

    'Attempt to delete a file
    Kill "AnyFile"

    'If no error occurred, do nothing. Otherwise, display
    'a message with the description of the error that occurred
    If Err.Number = 0 Then
    Else
        MsgBox "We Didn't Die, But the Error Was: " & Err.Description
    End If

    Exit Sub
TestResumeNextInError_Err:
    'Reset error information and resume execution on the
    'line of code following the line on which the error cocurred
    Resume Next
End Sub

In this example, the code is instructed to go to the label called TestResumeNextInError_Err when an error occurs. The TestResumeNextInError_Err label issues a Resume Next statement. This clears the error and causes execution to continue on the line after the line on which the error occurred.

Note

Note the difference between On Error Resume Next and Resume Next. On Error Resume Next is placed in the body of the routine. It causes code execution to continue on the line of code following the line that caused the error. It does not reset any error information.

Resume Next is placed within the error handler. It also causes code execution to continue on the line of code following the line that caused the error. It does reset the error information.

The Resume <LineLabel> Statement

The Resume <LineLabel> statement enables you to specify a line of code where you want code execution to continue after an error occurs. This is a great way to eliminate the two Exit Sub or Exit Function statements required by the error-handling routines you have looked at so far. Listing 16.8 shows an example.

Example 16.8. Using the Resume <LineLabel> Statement to Specify Where Execution Continues After an Error Occurs

Sub TestResumeLineLabel(intVar1 As Integer, intVar2 As Integer)
    'Invoke error handling
    On Error GoTo TestResumeLineLabel_Err

    Dim sngResult As Single
    'Divide the value received as the first parameter
    'by the value received as the second parameter
    sngResult = intVar1 / intVar2
TestResumeLineLabel_Exit:
    'Exit subroutine
    Exit Sub

TestResumeLineLabel_Err:
    'If an error occurs, display message with the error
    'number and description
    MsgBox "Error #" & Err.Number & ": " & Err.Description

    'Resume execution at the TestResumeLineLable_Exit label
    Resume TestResumeLineLabel_Exit

End Sub

Notice that this routine contains only one Exit Sub statement. If no error occurs, Access drops through the TestResumeLineLabel_Exit label to the Exit Sub statement. If an error does occur, the code in the TestResumeLineLabel_Err label executes. Notice that the last line of the error label resumes execution at the TestResumeLineLabel_Exit label.

This method of resolving an error is useful because any code required to execute as the routine is exited can be written in one place. Object variables might need to be set equal to Nothing as the routine is exited, for example. You can place these lines of code in the exit routine.

Clearing an Error

When an error occurs, the Err object remains set with the error information until one of the following clears the error:

  • Resume, Resume Next, or Resume <LineLabel>

  • Exit Sub, Exit Function, or Exit Property

  • Any Goto statement

  • Explicitly using the Clear method on the Err object

Until the error is cleared somehow, all the information remains set in the Err object. After the error is cleared, no information is found in the Err object.

Examining the Cascading Error Effect

As mentioned earlier in the section “Using On Error Statements,” if Access does not find any error handling in a particular subroutine or function, it looks up the call stack for a previous error handler. Listing 16.9 shows an example of this process.

Example 16.9. Looking Up the Call Stack for a Previous Error Handler

Sub Func1()

    'Invoke error handling
    On Error GoTo Func1_Err

    'Print to the immediate window
    Debug.Print "I am in Function 1"

    'Execute the Func2 routine
    Call Func2

    'Print to the immediate window
    Debug.Print "I am back in Function 1"

    'Exit the subroutine
    Exit Sub

Func1_Err:
    'Display a message to the user,
    'indicating that an error occurred
    MsgBox "Error in Func1"

    'Resume execution
    Resume Next
End Sub

Sub Func2()
    'No error handling in this routine!
    'Print to the immediate window
    Debug.Print "I am in Func2"

    'Execute Func3
    Call Func3

    'Print to the immediate window
    Debug.Print "I am still in Func2"
End Sub

Sub Func3()
    'No error in this routine either!
    Dim sngAnswer As Single

    'Print to the immediate window
    Debug.Print "I am in Func3"
    'Opps, an error occurred
    sngAnswer = 5 / 0

    'This line of code will never execute
    Debug.Print "I am still in Func3"
End Sub

In this situation, the error occurs in Func3. Because Func3 does not have its own error handling, it refers back to Func2. Func2 does not have any error handling either, so Func2 relinquishes control to Func1. VBA executes the error code in Func1. The real problem occurs because of the Resume Next statement. The application continues executing within Func1 on the Debug.Print "I am back in Function 1" statement. This type of error handling is dangerous and confusing. Therefore, it is best to develop a generic error-handling routine that is accessed throughout your application. The creation of a generic error handler is discussed in the section “Creating a Generic Error Handler.”

Using the Err Object

The Err object contains information about the most recent error that occurred. As with all Access objects, it has its own built-in properties and methods. Table 16.1 lists the properties of the Err object.

Table 16.1. Properties of the Err Object

Property

Description

Description

Description of the error that occurred

HelpContext

Context ID for the Help file

HelpFile

Path and filename of the Help file

LastDLLError

Last error that occurred in a 32-bit DLL

Number

Number of the error that was set

Source

System in which the error occurred (which is extremely useful when you are using OLE automation to control another application, such as Excel)

The Err object has only two methods: Clear and Raise. The Clear method enables you to clear an error condition explicitly. This is used primarily when you write code that uses the On Error Resume Next statement. This statement does not clear the error condition. Remember that there is no reason to issue the Clear method explicitly with any type of Resume, Exit Sub, Exit Function, Exit Property, or On Error Goto statement. The Clear method is implicitly issued when these constructs are used. The Raise method of the Err object is covered in the next section.

Raising an Error

You use the Raise method of the error object in these situations:

  • When you want to generate an error on purpose (for example, in testing)

  • When you want to generate a user-defined error

  • When no code in the error routine handles the current error, and you want to allow other parts of the call stack to attempt to handle the error

  • When you want to nest an error handler

Using the Raise method to generate an error on purpose and creating a user-defined error are covered in the following sections.

Generating an Error on Purpose

Many times during testing, you want to generate an error so that you can check your own error handling. Instead of figuring out how to cause the error condition, you can use the Raise method of the Err object to accomplish this task, as Listing 16.10 shows.

Example 16.10. Raising an Error

Sub TestRaiseError()
    'Invoke error handling
    On Error GoTo TestRaiseError_Err

    Dim sngResult As String

    'Raise a divide-by-zero error
    Err.Raise 11

    'Exit the subroutine
    Exit Sub

TestRaiseError_Err:
    'Display a message with the error number and description
    MsgBox "Error #" & Err.Number & ": " & Err.Description

    'Exit the subroutine
    Exit Sub

End Sub

This code invokes an error 11 (divide by 0).

Creating User-Defined Errors

Another important use of the Raise method of the Err object is the generation of a custom error condition. This is useful when you want to force an error in response to something that the user did. For example, assume that the user must enter five characters into an unbound text box. Entering only two characters would not generate an Access error. Rather than handling this user-generated error in some other manner, you can raise the error and have your standard error handler respond to the error condition. Because the Raise method enables you to set all the properties of the Err object, you can create a user-defined error complete with a number, description, source, and so on, as shown in Listing 16.11.

Example 16.11. Creating a User-Defined Error

Sub TestCustomError()
    'Invoke error handling
   On Error GoTo TestCustomError_Err
   Dim strName As String

   'Prompt the user to enter their name
   strName = InputBox("Please Enter Your Name")

   'If the length of the name is less than five
   'characters, raise an error number 11111
   If Len(strName) < 5 Then
      Err.Raise Number:=11111, _
               Description:="Length of Name is Too Short"
   Else
      MsgBox "You Entered " & strName
   End If

   Exit Sub

TestCustomError_Err:
    'Display a message with the error number
    'and description
    MsgBox "Error # " & Err.Number & _
        " - " & Err.Description
    Exit Sub
End Sub

Although it is very simple, Listing 16.11 illustrates an important use of generating user-defined errors. The code tests to see whether the value entered has less than five characters. If it does, a user-defined error message (number 11111) is generated. The routine drops into the normal error-handling routine. The section “Creating a Generic Error Handler,” later in this chapter, explores how to put together a generic error handler. By passing user-defined errors through your generic error handler, all errors—user-defined or not—are handled in the same way.

Using the Errors Collection

The Errors collection is part of Access’s Jet Engine. It stores the most recent set of DAO errors that have occurred. This is important when dealing with DAO (Data Access Objects) and ODBC (Open Database Connectivity), in which one operation can result in multiple errors. If you are concerned with each error that was generated by one operation, you need to look at the Errors collection. Each error object in the Errors collection contains information about an error that occurred. If you want to view the errors stored in the Errors collection, you must loop through it, viewing the properties of each Err object. Listing 16.12 shows the code you can use to accomplish this.

Example 16.12. Viewing the Errors Stored in the Errors Collection

Sub TestErrorsCollection()
    'Invoke error handling
    On Error GoTo TestErrorsCollection_Err

    'Declare a DAO database object
    Dim db As DAO.Database

    'Point the database object at the database
    'referenced by the CurrentDB object
    Set db = CurrentDb

    'Attempt to execute a query that doesn't exist
    db.Execute ("qryNonExistent")

    Exit Sub

TestErrorsCollection_Err:

    Dim ErrorDescrip As DAO.Error

    'Loop through the errors collection,
    'sending the error number and description to
    'the Immediate window
    For Each ErrorDescrip In Errors
        Debug.Print ErrorDescrip.Number
        Debug.Print ErrorDescrip.Description
    Next ErrorDescrip
    Exit Sub
End Sub

This routine loops through each Error object in the Errors collection, printing the description of each error contained in the collection.

Creating a Generic Error Handler

A generic error handler can be called from every procedure in your application to respond to any type of error.

A generic error handler prevents you from having to write specific error handling in each of your subroutines and functions. This enables you to invoke error handling throughout your application in the most efficient manner possible.

You can take many approaches to create a generic error handler. It should give users information about the error, enable users to print this information, and log the information to a file.

The On Error routine (in this case, the label AnySub_Err) of every procedure that performs error handling should look like the error-handling routine contained in the subroutine in Listing 16.13.

Example 16.13. A Generic Error Handler for All Your Functions and Subroutines

Sub AnySub()
'Declare constant with the name of the routine
Const SUBNAME As String = "AnySub"

'Invoke error handling
On Error GoTo AnySub_Err

    'Beginning of any routine
    MsgBox "This is the rest of your code...."

    'Oops! Something causes an error!
    Err.Raise 11

    'Code after the error
    MsgBox "We are Past the Error!!"

AnySub_Exit:
    'Generic exit point for routine
    Exit Sub

AnySub_Err:
    Dim intAction As Integer
    'Call generic error handler, passing it the error
    'number and description, as well as the module name
    'and subroutine name
    intAction = ErrorHandler(lngErrorNum:=Err.Number, _
                  strErrorDescription:=Err.Description, _
                  strModuleName:=MODULENAME, _
                  strRoutineName:=SUBNAME)

    'Evaluate return value to determine what action to take
    Select Case intAction
        Case ERR_CONTINUE
            Resume Next
        Case ERR_RETRY
            Resume
        Case ERR_EXIT
            Resume AnySub_Exit
        Case ERR_QUIT
            Quit
    End Select
End Sub

This error-handling routine in AnySub creates an Integer variable that holds the return value from the error system. The intAction variable holds an appropriate response to the error that occurred. The error routine calls the generic error-handling function ErrorHandler, passing it the error number (Err.Number), a description of the error (Err.Description), the name of the module containing the error, and the name of the subroutine or function containing the error. The name of the module is stored in a Private constant called MODULENAME. The Private constant is declared in the General section of the module and needs to be created for every module you make. The name of the subroutine or function is stored in a local constant called SUBNAME. With this approach, you create a local constant and assign it the name of the sub at the beginning of each procedure. This requires upkeep because procedure names can change, and you need to remember to change your string. Unfortunately, because the VBA environment does not expose the subroutine and module names to you when an error occurs, this sort of brute force is necessary if you want your error handler to track the subroutine and module. When the code returns from the ErrorHandler function, a return value is placed in the intAction variable. This return value is used to determine the fate of the routine.

Tip

The process of adding error handling to every routine that you create is a tedious one. Fortunately, a tool is available that makes the process much more palatable. The tool is produced by FMS Software and is called FMS Total Access Code Tools. This tool is covered in detail in Chapter 31, “Third-Party Tools That Can Help You to Get Your Job Done Effectively.” Amongst its many features, it allows you to create a custom error template that you can use to quickly and easily add error handling to all the routines that you create. A scaled-down version of FMS Total Access Code Tools is built into the Microsoft Office Developer.

Now that you have seen how to implement error handling in your procedures, take a look at the function that is called when an error occurs, as shown in Listing 16.14.

Example 16.14. A Type Structure Declaration to Be Used for Generic Error Handling

'Type structure used to hold error information
Type typErrors
    lngErrorNum As Long
    strMessage As String
    strModule As String
    strRoutine As String
    strUserName As String
    datDateTime As Variant
End Type

'Declaration of public type structure variable
Public gtypError As typErrors

'Constants used by global error handler
Public Const ERR_CONTINUE = 0 'Resume Next
Public Const ERR_RETRY = 1 'Resume
Public Const ERR_QUIT = 2  'End
Public Const ERR_EXIT = 3  'Exit Sub or Func

This code is placed in the General section of basHandleErrors. The type structure declared holds all the pertinent information about the error. A type structure is a special kind of variable that is made up of various parts, each of which stores a different piece of information. (Type structures are covered in Chapter 12, “Advanced VBA Techniques.”)

In Listing 16.14, the public variable gtypError holds all the information from the type structure. The constants are used to help determine the fate of the application after an error occurs. Listing 16.15 shows the ErrorHandler function.

Example 16.15. Using the ErrorHandler Function

Function ErrorHandler(lngErrorNum As Long, _
                  strErrorDescription As String, _
                  strModuleName As String, _
                  strRoutineName As String) As Integer


    'Populate elements of the type structure variable
    'with information about the error that occurred
    gtypError.lngErrorNum = lngErrorNum
    gtypError.strMessage = strErrorDescription
    gtypError.strModule = strModuleName
    gtypError.strRoutine = strRoutineName
    gtypError.strUserName = CurrentUser()
    gtypError.datDateTime = Now

    'Log the error
    Call LogError

    'Locate the error number in tblErrors to
    'determine how you should respond to the error
    Dim rst As adodb.Recordset
    Set rst = New adodb.Recordset
    rst.Open "Select Response from tblErrors Where ErrorNum = " & _
    lngErrorNum, CurrentProject.Connection, adOpenStatic

    'If the error number that occurred is not found
    'in tblErrors, display the error form and return
    'ERR_QUIT to the problem routine
    If rst.EOF Then
        DoCmd.OpenForm "frmError", WindowMode:=acDialog, _
            OpenArgs:="ErrorHandler"
        ErrorHandler = ERR_QUIT

    'If the error is in tblErrors, evaluate the contents of
    'the Response field. Response appropriately, displaying the appropriate
    'form and returning the appropriate value to the offending routine
    Else
        Select Case rst!Response
            Case ERR_QUIT
                 DoCmd.OpenForm "frmError", WindowMode:=acDialog, _
                     OpenArgs:="Critical Error:  Application will Terminate"
                 ErrorHandler = ERR_QUIT
            Case ERR_RETRY
                 ErrorHandler = ERR_RETRY
            Case ERR_EXIT
                 DoCmd.OpenForm "frmError", WindowMode:=acDialog, _
                     OpenArgs:="Severe Error:  Processing Did Not Complete"
                 ErrorHandler = ERR_EXIT
            Case ERR_CONTINUE
                 ErrorHandler = ERR_CONTINUE
         End Select
    End If

End Function

The ErrorHandler function receives the error number, error description, module name, and subroutine or function name as parameters. It then fills in the gtypError type structure with the information that it was passed, as well as the current user and date. Next, it calls a routine that logs the error into an Access table. The routine looks up the severity of the error code in an Access table called tblErrors to decide the most appropriate way to handle the error. If the error code is not found in the error table, an error form is displayed, and a return value is sent to the calling function, indicating that application execution is to be terminated. If the error code is found in the tblErrors table and determined to be critical or severe, an error form appears before control is returned to the calling routine. In any case, a severity code for the error is returned to the calling function. The following section discusses the details involved in each step of the process.

Logging the Error

The LogError routine is responsible for logging all the error information into an Access table. Because users often decide not to print the error form or provide you with inaccurate information about what was happening when the error occurred (or neglect to tell you about the error), it is important that you log each error so that you can review the error log at any time. Errors can be logged to a text file or a data table. This section shows you both methods of logging your errors. Start with logging your errors to a table, as shown in Listing 16.16 with the LogError routine.

Example 16.16. Using the LogError Routine

Sub LogError()

    'Declare a Connection object
    Dim cnn As adodb.Connection
    Dim strSQL As String

    'Point the Connection object at the connection
    'associated with the current project
    Set cnn = CurrentProject.Connection

    'Build a SQL statement that inserts error information
    'into the tblErrorLog table
    strSQL = "INSERT INTO tblErrorLog ( ErrorDate, ErrorTime, UserName, ErrorNum,
Using the LogError Routine ErrorString, ModuleName, RoutineName) "
    strSQL = strSQL & "Select #" & gtypError.datDateTime & "#, #" _
                              & gtypError.datDateTime & "#, '" _
                              & gtypError.strUserName & "', " _
                              & gtypError.lngErrorNum & ", '" _
                              & gtypError.strMessage & "', '" _
                              & gtypError.strModule & "', '" _
                              & gtypError.strRoutine & "'"

    'Execute the SQL statement
    cnn.Execute strSQL, , adExecuteNoRecords

End Sub

This routine uses the Execute method of the ADO Connection object to add a record to your error table. The record contains all the information from the structure called gtypError. The information is logged to a table called tblErrorLog. Figure 16.4 shows the structure of this table.

The structure of the tblErrorLog table.

Figure 16.4. The structure of the tblErrorLog table.

The alternative error-logging method is to write the information to a textual error log file, as shown in Listing 16.17.

Example 16.17. Writing Information to a Textual Error Log File

Sub LogErrorText()
    Dim intFile As Integer

    'Store a free file handle into a variable
    intFile = FreeFile
    'Open a file named ErrorLog.txt in the current directory
    'using the file handle obtained above
    Open CurDir & "ErrorLog.Txt" For Append Shared As intFile

    'Write the error information to the file
    Write #intFile, "LogErrorDemo", Now, Err, Error, CurrentUser()

    'Close the file
    Close intFile
End Sub

This code uses low-level file functions to Open and Write to an ASCII text file. All the pertinent information about the error is written to this text file. The routine then uses the Close command to close the text file. The potential advantage of this routine is that, if the problem is with the database (for example, the network is down), the error logging process still succeeds.

Determining the Appropriate Response to an Error

After the error is logged, you are ready to determine the best way to respond to the error. By making your error system data driven, you can handle each error a little differently. Figure 16.5 shows the structure of the tblErrors table. This table should contain a list of all the error numbers that you want to trap. It contains three fields: ErrorNum, ErrorDescription, and Response. When an error occurs, the ErrorHandler function searches for a record with a value in the ErrorNum field that matches the number of the error that occurred.

The structure of the tblErrors table.

Figure 16.5. The structure of the tblErrors table.

The ErrorHandler function, as shown in Listing 16.15, uses the code in Listing 16.18 to locate the error code in the tblErrors table.

Example 16.18. Locating the Error Code in the tblErrors Table

    'Locate the error number in tblErrors to
    'determine how you should respond to the error
    Dim rst As adodb.Recordset
    Set rst = New adodb.Recordset
    rst.Open "Select Response from tblErrors Where ErrorNum = " & lngErrorNum, _
        CurrentProject.Connection, adOpenStatic

    'If the error number that occurred is not found
    'in tblErrors, display the error form and return
    'ERR_QUIT to the problem routine
    If rst.EOF Then
        DoCmd.OpenForm "frmError", WindowMode:=acDialog, _
            OpenArgs:="ErrorHandler"
        ErrorHandler = ERR_QUIT

    'If the error is in tblErrors, evaluate the contents of
    'the Response field. Respond appropriately, displaying the appropriate
    'form and returning the appropriate value to the offending routine
    Else
        Select Case rst!Response
            Case ERR_QUIT
                 DoCmd.OpenForm "frmError", WindowMode:=acDialog, _
                     OpenArgs:="Critical Error:  Application will Terminate"
                 ErrorHandler = ERR_QUIT
            Case ERR_RETRY
                 ErrorHandler = ERR_RETRY
            Case ERR_EXIT
                 DoCmd.OpenForm "frmError", WindowMode:=acDialog, _
                     OpenArgs:="Severe Error:  Processing Did Not Complete"
                 ErrorHandler = ERR_EXIT
            Case ERR_CONTINUE
                 ErrorHandler = ERR_CONTINUE
         End Select
    End If

The part of the ErrorHandler function shown in Listing 16.18 creates an ADO Recordset object variable. It opens a recordset using a Select statement, which in turn searches a table called tblErrors. If a match is found, the Response column is used to determine the response to the error. Notice in Listing 16.18 that, if the error number is not found in tblErrors, default error handling occurs, which means that the code handles all other errors as a group. (This is my default error handling, not Access’s.) If the error number is found, the Response field is evaluated and the appropriate action is taken (via the Case statement). If it is not found, the frmError form is opened, and the ERR_QUIT constant value is returned from the ErrorHandler function. When using this method, you need to add to the table only specific errors that you want to trap.

If the error number is found in tblErrors, the Response field from the recordset is evaluated. If the Response field contains the constant value ERR_QUIT or ERR_EXIT, the frmError form appears before the constant value is returned to the offending function or subroutine. If the Response field contains the constant value for ERR_RETRY or ERR_CONTINUE, the constant value is returned without displaying the frmError form.

Note

The tblErrors table is included in CHAP16EX.MDB on the sample code CD-ROM. To take full advantage of this table, you must add all errors that you want to trap, along with the actions that you want the error handler to take when that error occurs.

Note

Listing 16.19 shows how the return value from the ErrorHandler function is used.

Example 16.19. Using the Return Value from the ErrorHandler Function

Sub AnySub()
'Declare constant with the name of the routine
Const SUBNAME As String = "AnySub"

'Invoke error handling
On Error GoTo AnySub_Err

    'Beginning of any routine
    MsgBox "This is the rest of your code...."

    'Oops! Something causes an error!
    Err.Raise 11

    'Code after the error
    MsgBox "We are Past the Error!!"

AnySub_Exit:
    'Generic exit point for routine
    Exit Sub

AnySub_Err:
    Dim intAction As Integer

    'Call generic error handler, passing it the error
    'number and description, as well as the module name
    'and subroutine name
    intAction = ErrorHandler(lngErrorNum:=Err.Number, _
                  strErrorDescription:=Err.Description, _
                  strModuleName:=MODULENAME, _
                  strRoutineName:=SUBNAME)

    'Evaluate return value to determine what action to take
    Select Case intAction
        Case ERR_CONTINUE
            Resume Next
        Case ERR_RETRY
            Resume
        Case ERR_EXIT
            Resume AnySub_Exit
        Case ERR_QUIT
            Quit
    End Select
End Sub

In Listing 16.19, the AnySub routine generates an error 11 (divide by 0). Because tblErrors contains the number 3 in the Response column and the ERR_CONTINUE constant is equal to 3, the error form is displayed, and the AnySub routine is exited with an Exit Sub statement.

Note

To test what happens when the error code is not found in the tblErrors table, run the SubWithUnknownError routine found in basError. To test what happens when the ERR_CONTINUE code is returned, execute the SubWithContinue routine.

Creating an Error Form

The code in the error form’s Load event calls two subroutines: GetSystemInfo and GetErrorInfo, as shown here:

Private Sub Form_Load()

    'Call routine to obtain system information
    Call GetSysInfo(Me)

    'Call routine to obtain error information
    Call GetErrorInfo(Me)

    'If FormCaption property contains a value, use the
    'value as the caption for the form
    If Not IsNull(Me.OpenArgs) Then
        Me.lblAction.Caption = Me.OpenArgs
    End If
End Sub

The first subroutine is called GetSysInfo. It performs several Windows API calls to fill in the system information on your form. The code is shown in Listing 16.20, and it is discussed in Chapter 23, “Exploiting the Power of the Windows API.”

Example 16.20. Getting System Information Through Code

Sub GetSysInfo(frmAny As Form)

    'Get Free Memory
    Dim MS As MEMORYSTATUS
    MS.dwLength = Len(MS)
    GlobalMemoryStatus MS
    frmAny.lblMemoryTotal.Caption = Format(MS.dwTotalPhys, "Standard")
    frmAny.lblMemoryAvail.Caption = Format(MS.dwAvailPhys, "Standard")

    'Get Version Information
    Dim OSInfo As OSVERSIONINFO
    OSInfo.dwOSVersionInfoSize = Len(OSInfo)
    If GetVersionEx(OSInfo) Then
        frmAny.lblOSVersion.Caption = OSInfo.dwMajorVersion & "." & OSInfo.dwMinorVersion
        frmAny.lblBuild.Caption = OSInfo.dwBuildNumber And &HFFFF&
        If OSInfo.dwPlatformId = 0 Then
            frmAny.lblPlatform.Caption = "Windows 95"
        Else
            frmAny.lblPlatform.Caption = "Windows NT"
        End If
    End If

    'Get System Information
    Dim SI As SYSTEM_INFO
     GetSystemInfo SI
    frmAny.lblProcessor.Caption = SI.dwProcessorType

End Sub

These API calls require the Declare statements and constants shown in Listing 16.21. They are placed in a module called basAPI.

Example 16.21. Declaring Windows API Calls

'Declarations required by WinAPI Calls
Option Compare Database
Option Explicit

Private Declare Sub GlobalMemoryStatus Lib "Kernel32"_
(lpBuffer As MEMORYSTATUS)

Private Type MEMORYSTATUS
   dwLength As Long
   dwMemoryLoad As Long
   dwTotalPhys As Long
   dwAvailPhys As Long
   dwTotalPageFile As Long
   dwAvailPageFile As Long
   dwTotalVirtual As Long
   dwAvailVirtual As Long
End Type

Private Declare Function GetVersionEx Lib "Kernel32" _
  Alias "GetVersionExA" (lpOSInfo As OSVERSIONINFO) As Boolean

Type OSVERSIONINFO
   dwOSVersionInfoSize As Long
   dwMajorVersion As Long
   dwMinorVersion As Long
   dwBuildNumber As Long
   dwPlatformId As Long
   strReserved As String * 128
End Type

Private Declare Sub GetSystemInfo Lib "Kernel32"_
  (lpSystemInfo As SYSTEM_INFO)

Private Type SYSTEM_INFO
   dwOemID As Long
   dwPageSize As Long
   lpMinimumApplicationAddress As Long
   lpMaximumApplicationAddress As Long
   dwActiveProcessorMask As Long
   dwNumberOrfProcessors As Long
   dwProcessorType As Long
   dwAllocationGranularity As Long
   dwReserved As Long
End Type

The second subroutine, GetErrorInfo, fills in the labels on the error form with all the information from your structure, as shown in Listing 16.22.

Example 16.22. Using the GetErrorInfo Subroutine

Sub GetErrorInfo(frmAny As Form)
    'Populate form controls with error information
    'contained in the type variable
    frmAny.lblErrorNumber.Caption = gtypError.lngErrorNum
    frmAny.lblErrorString.Caption = gtypError.strMessage
    frmAny.lblUserName.Caption = gtypError.strUserName
    frmAny.lblDateTime.Caption = Format(gtypError.datDateTime, "c")
    frmAny.lblModuleName.Caption = gtypError.strModule
    frmAny.lblRoutineName.Caption = gtypError.strRoutine
End Sub

Finally, the disposition of the error, sent as an OpenArg from the ErrorHandler function, is displayed in a label on the form. Figure 16.6 shows the error form.

The frmErrors form displays important information about the error that occurred.

Figure 16.6. The frmErrors form displays important information about the error that occurred.

Printing the Error Form

Users often are not very accurate in describing an error and corresponding error message. It therefore is important to give them the capability to print their error message. The code in Listing 16.23 prints your error form. It is found behind the click event of the Print button on the error form.

Example 16.23. Printing an Error Form

Sub cmdPrint_Click()
On Error GoTo Err_cmdPrint_Click

    'Use the PrintOut method to print the form
    DoCmd.PrintOut

Exit_cmdPrint_Click:
    Exit Sub

Err_cmdPrint_Click:
    MsgBox Err.Description
    Resume Exit_cmdPrint_Click

End Sub

Preventing Your Own Error Handling from Being Invoked

When you are testing your application, you do not want your own error handling to be triggered. Instead, you want VBA’s error handling to be activated. The trick is in the Options dialog box of the VBE. Choose Tools|Options and click the General tab. Enable the option Break on All Errors located in the Error Trapping section. As long as this option is set, your error handling is ignored, and Access’s default error handling is invoked. Using this setting, you can turn error handling on and off from one central location.

Other settings for error trapping are Break in Class Module and Break on Unhandled Errors (the default). With the latter setting, handled errors do not cause the application to enter break mode. Unhandled errors do cause the project to enter break mode and place you, or the user, on the line of code that invoked the error handler. The Break in Class Module option causes only unhandled errors in a class module to invoke break mode.

Creating a Call Stack

While in the debugger, it is very easy to view the call stack. Unfortunately, the call stack information cannot be accessed programmatically when an error occurs. If you wish to keep track of the sequence of procedures that brought you to the error condition, you must do it yourself. The code in Listing 16.24 shows three routines. Func1 calls Func2, and then Func2 calls Func3. Func3 renders an error.

Example 16.24. Routines That Call One Another

Sub Func1()

    'Invoke error handling
    On Error GoTo Func1_Err

    'Put routine in call stack
    ERH_PushStack_TSB ("Func1")

    'Print to the immediate window
    Debug.Print "I am in Function 1"

    'Execute the Func2 routine
    Call Func2

    'Print to the immediate window
    Debug.Print "I am back in Function 1"
Func1_Exit:

    'Pop error stack
    ERH_PopStack_TSB

    'Exit the subroutine
    Exit Sub

Func1_Err:
    'Display a message to the user,
    'indicating that an error occurred
    MsgBox "Error in Func1"

    'Resume execution
    Resume Func1_Exit
End Sub

Sub Func2()
    'Put routine in call stack
    ERH_PushStack_TSB ("Func2")

    On Error GoTo Func2_Err

    Debug.Print "I am in Func2"

    'Execute Func3
    Call Func3

    'Print to the immediate window
    Debug.Print "I am still in Func2"

Func2_Exit:

    'Pop error stack
    ERH_PopStack_TSB

    'Exit the subroutine
    Exit Sub

Func2_Err:
    'Display a message to the user,
    'indicating that an error occurred
    MsgBox "Error in Func1"
    'Resume execution
    Resume Func2_Exit

End Sub

Sub Func3()
    Dim sngAnswer As Single

    'Put routine in call stack
    ERH_PushStack_TSB ("Func3")

    On Error GoTo Func3_Err

    'Print to the immediate window
    Debug.Print "I am in Func3"

    'Opps, an error occurred
    sngAnswer = 5 / 0

    'This line of code will never execute
    Debug.Print "I am still in Func3"

Func3_Exit:

    'Pop error stack
    ERH_PopStack_TSB

    'Exit the subroutine
    Exit Sub

Func3_Err:
    Dim intCounter As Integer
    Dim strCallStack As String

    For intCounter = LBound(gaERH_Stack_TSB) To UBound(gaERH_Stack_TSB)
        If Len(gaERH_Stack_TSB(intCounter)) Then
            strCallStack = strCallStack & _
                gaERH_Stack_TSB(intCounter) & vbCrLf
        End If
    Next intCounter

    MsgBox Err.Number & ": " & Err.Description & _
        vbCrLf & strCallStack

    Resume Func3_Exit
End Sub

Notice that at the beginning of each routine, the ERH_PushStack_TSB subroutine is called, as shown in Listing 16.25.

Example 16.25. The ERH_PushStack_TSB Function Pushes the Error into the Stack

Sub ERH_PushStack_TSB(strProc As String)
  ' Comments  : Pushes the supplied procedure name onto the error handling
stack
  ' Parameters: strProc - name of the currently executing procedure
  ' Returns   : Nothing
  '

  gintERH_Pointer_TSB = gintERH_Pointer_TSB + 1

  If gintERH_Pointer_TSB <= ERH_MAXITEMS_TSB Then
    gaERH_Stack_TSB(gintERH_Pointer_TSB) = strProc
  Else
    gaERH_Stack_TSB(gintERH_Pointer_TSB + 2) = strProc
  End If

End Sub

The code adds the name of the procedure to the gaERH Stack_TSB array. The ERH_PopStack_TSB subroutine, shown in Listing 16.26, is executed in the exit code for each procedure.

Example 16.26. The ERH_PopStack_TSB Function Removes the Error from the Stack

Sub ERH_PopStack_TSB()
  ' Comments  : Pops the current procedure name off the error handling stack
  ' Parameters: None
  ' Returns   : Nothing
  '
  If gintERH_Pointer_TSB <= ERH_MAXITEMS_TSB Then
    gaERH_Stack_TSB(gintERH_Pointer_TSB) = ""
  End If

  gintERH_Pointer_TSB = gintERH_Pointer_TSB - 1

  If gintERH_Pointer_TSB < 0 Then
    gintERH_Pointer_TSB = 0
  End If

End Sub

The ERH_PopStack_TSB subroutine removes the text in the largest array element.

Note

The ERH_PushStack_TSB and ERH_PullStack_TSB subroutines are found in the FMS SourceBook library. For more information about the FMS SourceBook, see Chapter 32.

As the code goes in and out of routines, entries are added to and then removed from the array. Because the array is Public, you can review its contents at any time. Notice in Func3 in Listing 16.24 that the error handler iterates through the array, pulling out the error information.

Building a Custom Error Handler Class

Implementing error handling within an application can be very tedious, especially if you attempt to place specific error-handling logic in each routine that you write. Although implementing a generic error handler does not mandate the use of a class module, using a class module greatly facilitates the process of implementing error handling within your applications. Listing 6.27 illustrates this point.

Example 16.27. An Example of an Access Subroutine

Sub AnySub2()
'Declare constant with the name of the routine
Const SUBNAME As String = "AnySub"

'Invoke error handling
On Error GoTo AnySub2_Err

    'Beginning of any routine
    MsgBox "This is the rest of your code...."

    'Oops! Something causes an error!
    Err.Raise 11

    'Code after the error
    MsgBox "We are Past the Error!!"

AnySub2_Exit:
    'Generic exit point for routine
    Exit Sub

AnySub2_Err:
    Dim intAction As Integer
    'Instantiate the error handler class
    Set gobjErrorHandler = New ErrorHandler

    'Execute the ErrorProcess method,
    'passing the error information
    intAction = gobjErrorHandler.ErrorProcess(ModuleName, _
        SUBNAME, Err.Number, Err.Description)

    'Evaluate return value to determine what action to take
    Select Case intAction
        Case ERR_CONTINUE
            Resume Next
        Case ERR_RETRY
            Resume
        Case ERR_EXIT
            Resume AnySub2_Exit
        Case ERR_QUIT
            Quit
    End Select
End Sub

When an error occurs, the ErrorHandler class is instantiated. The Initialize event of the class executes, as shown in Listing 16.28.

Example 16.28. The Initialize Event of the ErrorHandler Class

Private Sub Class_Initialize()
    'Place user name into private variable
    mstrUsername = CurrentUser

    'Place current date and time into private variable
    mdatDateTime = Now
End Sub

The Initialize event of the class sets the module-level variables mstrUserName and mdatDateTime equal to the CurrentUser and the current date and time, respectively. The Username and DateTime properties of the class use these variables.

The ErrorProcess method of the ErrorHandler class is then executed. It appears in Listing 16.29.

Example 16.29. The ErrorProcess Method of the ErrorHandler Class Logs the Error and Then Takes Appropriate Action in Response to the Error

Public Function ErrorProcess(strRoutine As String, _
    strModule As String, _
    lngErrorNumber As Long, _
    strErrorMessage As String) As Integer

    'Store error information into module-level variables
    mstrRoutine = strRoutine
    mstrModule = strModule
    mlngErrorNumber = lngErrorNumber
    mstrErrorMessage = strErrorMessage

    'Log error
    Call LogError

    'Locate the error number in tblErrors to
    'determine how you should respond to the error
    Dim rst As ADODB.Recordset
    Set rst = New ADODB.Recordset
    rst.Open "Select Response from tblErrors Where ErrorNum = " & lngErrorNumber, _
        CurrentProject.Connection, adOpenStatic

    'If the error number that occurred is not found
    'in tblErrors, display the error form and return
    'ERR_QUIT to the problem routine
    If rst.EOF Then
        DoCmd.OpenForm "frmError2", WindowMode:=acDialog, _
            OpenArgs:="ErrorHandler"
        ErrorProcess = ERR_QUIT

    'If the error is in tblErrors, evaluate the contents of
    'the Response field. Respond appropriately, displaying the appropriate
    'form and returning the appropriate value to the offending routine
    Else
        Select Case rst!Response
            Case ERR_QUIT
                 DoCmd.OpenForm "frmError2", WindowMode:=acDialog, _
                     OpenArgs:="Critical Error:  Application will Terminate"
                 ErrorProcess = ERR_QUIT
            Case ERR_RETRY
                 ErrorProcess = ERR_RETRY
            Case ERR_EXIT
                 DoCmd.OpenForm "frmError2", WindowMode:=acDialog, _
                     OpenArgs:="Severe Error:  Processing Did Not Complete"
                 ErrorProcess = ERR_EXIT
            Case ERR_CONTINUE
                 ErrorProcess = ERR_CONTINUE
         End Select
    End If

End Function

The routine first sets the ModuleName, Routine, ErrorMessage, and ErrorNumber variables within the class to the values of the parameters passed to the ErrorProcess method. The Property Get routines for the ModuleName, Routine, ErrorMessage, and ErrorNumber properties appear in Listing 16.30. Because we only want the properties to be set via the ErrorProcess method, no Property Let routines exist.

Example 16.30. The Property Get Routines of the Class Are Responsible for Manipulating Error Information

Public Property Get ModuleName() As String
    ModuleName = mstrModule
End Property

Public Property Get Routine() As String
    Routine = mstrRoutine
End Property

Public Property Get ErrorMessage() As String
    ErrorMessage = mstrErrorMessage
End Property

Public Property Get ErrorNumber() As Integer
    ErrorNumber = mlngErrorNumber
End Property

Public Property Get UserName() As String
    UserName = mstrUsername
End Property

Public Property Get DateTime() As Date
    DateTime = mdatDateTime
End Property

As you can see, the Property Get routines retrieve the values from their associated module-level variables.

Next, the function calls a routine that logs the error that occurred. This LogError routine is shown in Listing 16.31. The LogError routine utilizes ADO code to add a record to the tblErrorLog table. The record contains all the information about the error that occurred. Notice that the error information is retrieved from the module level variables populated by the ErrorHandler class’s ErrorProcess method.

After the error is logged, the number of the error that occurred is looked up in the tblErrors table. If it is not found in the tblErrors table, a form is displayed, containing all the critical information about the error that occurred. The value contained in the constant ERR_QUIT is returned from the ErrorHandler function. If the error number is found in the tblErrors table, the value contained in the Response field is evaluated. If it is the value contained in the constant ERR_QUIT, the frmError form is displayed, and the value in the constant ERR_QUIT is returned from the ErrorHandler method. If the Response field contains the value of the ERR_RETRY constant, that value is returned from the method, without the frmError form being displayed. If the Response field contains the value associated with the ERR_EXIT constant, the frmError form is displayed, and the ERR_EXIT value is returned from the ErrorHandler method. Finally, if the value in the Response field is the value associated with the ERR_CONTINUE constant, no error information is displayed, and the ERR_CONTINUE value is returned from the ErrorHandler method.

Example 16.31. The LogError Subroutine Uses ADO Code to Add the Error Information to tblErrorLog

Sub LogError()

    'Declare a Connection object
    Dim cnn As ADODB.Connection
    Dim strSQL As String

    'Point the Connection object at the connection
    'associated with the current project
    Set cnn = CurrentProject.Connection

    'Build a SQL statement that inserts error information
    'into the tblErrorLog table
    strSQL = "INSERT INTO tblErrorLog ( ErrorDate, ErrorTime, UserName, ErrorNum,
The LogError Subroutine Uses ADO Code to Add the Error Information to tblErrorLog ErrorString, ModuleName, RoutineName) "
    strSQL = strSQL & "Select #" & Me.DateTime & "#, #" _
                              & Me.DateTime & "#, '" _
                              & Me.UserName & "', " _
                              & Me.ErrorNumber & ", '" _
                              & Me.ErrorMessage & "', '" _
                              & Me.ModuleName & "', '" _
                              & Me.Routine & "'"

    'Execute the SQL statement
    cnn.Execute strSQL, , adExecuteNoRecords

End Sub

All the code contained in the ErrorHandler class is similar to that contained in the basErrorHandler module. The code has been modified so that it is implemented using properties and methods of a Class object.

The other code that is changed to utilize classes is the code behind the error form. Listing 16.32 shows the load event of the error form.

Example 16.32. The Form_Load Event Is Modified to Call Methods of the Appropriate Classes

Private Sub Form_Load()

    Dim objSys As SystemInformation
    Set objSys = New SystemInformation

    'Call routine to obtain system information
    Call objSys.GetSysInfo(Me)

    'Call routine to obtain error information
    Call gobjErrorHandler.GetErrorInfo(Me)

    'If FormCaption property contains a value, use the
    'value as the caption for the form
    If Not IsNull(Me.OpenArgs) Then
        Me.lblAction.Caption = Me.OpenArgs
    End If
End Sub

Notice that instead of calling the GetSysInfo function and the GetErrorInfo function, the load event executes the GetSysInfo method of the SystemInformation object and the GetErrorInfo method of the ErrorHandler object.

The GetSystemInfo function and associated declarations were moved to a SystemInformation class. No other changes were made to the code.

The GetErrorInfo function was moved to the ErrorHandler class and modified as shown in Listing 16.33.

Example 16.33. The GetErrorInfo method of the ErrorHandler Class Retrieves Properties of the Class

Sub GetErrorInfo(frmAny As Form)
    'Populate form controls with error information
    'contained in the type variable
    frmAny.lblErrorNumber.Caption = Me.ErrorNumber
    frmAny.lblErrorString.Caption = Me.ErrorMessage
    frmAny.lblUserName.Caption = Me.UserName
    frmAny.lblDateTime.Caption = Format(Me.DateTime, "c")
    frmAny.lblModuleName.Caption = Me.ModuleName
    frmAny.lblRoutineName.Caption = Me.Routine
End Sub

Notice that instead of using a type structure, the code references its own properties. The Private variables associated with these properties were set by the ErrorProcess method of the class.

Working with Error Events

Every form and report contains an error event procedure. This event is triggered by any interface or Jet Database Engine error. It is not triggered by a programming error made by the Access developer.

Errors often occur in the interface of a form or report, as well as in the Jet Database Engine. A user might try to enter an order for a customer who doesn’t exist, for example. Instead of displaying Access’s default error message, you might want to intercept and handle the error in a particular way.

After an error occurs within a form, its error event is triggered. In Listing 16.34, you can see Sub Form_Error. It contains two parameters. The first parameter is the number of the error. The second is the way you want to respond to the error. The error number is an Access-generated number.

This code, which is located in the frmOrders form in the CHAP16EX.MDB database, tests to see whether a referential integrity error has occurred. If it has, a message box asks whether the user wants to add the customer. If the user answers Yes, the customer form is displayed.

Example 16.34. Viewing Sub Form_Error From the Form frmOrders

Private Sub Form_Error(DataErr As Integer, Response As Integer)
    Dim intAnswer As Integer
    If DataErr = 3201 Then 'Referential Integrity Error
        intAnswer = MsgBox("Customer Does Not Exist...  _
                  Would You Like to Add Them Now", vbYesNo)
        If intAnswer = vbYes Then
            DoCmd.OpenForm "frmCustomer", , , , acAdd, acDialog
        End If
    End If
    Response = acDataErrContinue
End Sub

Caution

Be aware that the code in Listing 16.34 only traps referential integrity errors. It does not handle any other error.

The Response = acDataErrContinue line is very important. It instructs Access to continue the code execution without displaying the standard error message. The other option for Response is AcDataErrDisplay. It tells Access to display the default error message.

Creating a List of Error Codes and Descriptions

Many people ask me how to create a list of error numbers and descriptions. The code in Listing 16.35 creates a table of all the errors that can occur in your VBA code, with a description of what each error number means. You can copy this code into any module and run it.

Example 16.35. Code That Creates a Table of Errors and Descriptions

Sub CreateErrorsTable()
    Dim cnn As ADODB.Connection
    Dim rst As New ADODB.Recordset
    Dim lngCode As Long
    Const conAppObjectError = "Application-defined or object-defined error"

    Set cnn = CurrentProject.Connection
    ' Open recordset on Errors table.
    rst.Open "tblErrorMessages", cnn, adOpenStatic, adLockOptimistic
    ' Loop through first 10000 Visual Basic error codes.
    For lngCode = 1 To 10000
        On Error Resume Next
        ' Raise each error.
        Err.Raise lngCode
        DoCmd.Hourglass True
        ' Skip error codes that generate application or object-defined errors.
        If Err.Description <> conAppObjectError Then
            ' Add each error code and string to Errors table.
            rst.AddNew
            rst!ErrorCode = Err.Number
            rst!ErrorString = Err.Description
            rst.Update
        End If
        ' Clear Err object.
        Err.Clear
    Next lngCode
    ' Close recordset.
    rst.Close
    DoCmd.Hourglass False
    MsgBox "Errors table created."
End Sub

The code opens a recordset based on the tblErrorMessages table. It loops through from 1 to 10000, raising an error with each number. Each time through the loop, it appends the error number and the associated error description to the tblErrorMessages table.

Example 16.36. Incorporating the Generic Error Handler into Your Code

Sub GetCompanyInfo()
    Dim strSubName As String
    Dim rst As ADODB.Recordset
    'Declare constant with the name of the routine
    Const SUBNAME As String = "GetCompanyInfo"

    'Invoke error handling
    On Error GoTo GetCompanyInfo_Err

    'Instantiate the CompanyInformation class
    Set gobjCompanyInfo = New CompanyInformation
    Set rst = New ADODB.Recordset

    'Open a recordset based on the tblCompanyInfo table
    rst.Open "tblCompanyInfo", CurrentProject.Connection

    'Populate the properties of the public class instance
    'with values from the tblCompanyInfo table
    With gobjCompanyInfo
        .SetupID = rst!SetupID
        .CompanyName = rst!CompanyName
        .Address = rst!Address
        .City = rst!City
        .StateProvince = rst!StateProvince
        .PostalCode = rst!PostalCode
        .Country = rst!Country
        .PhoneNumber = rst!PhoneNumber
        .FaxNumber = rst!PhoneNumber
    End With

    rst.Close

GetCompanyInfo_Exit:
    'Generic exit point for routine
    Exit Sub

GetCompanyInfo_Err:
    Dim intAction As Integer

    'Instantiate the error handler class
    Set gobjErrorHandler = New ErrorHandler

    'Execute the ErrorProcess method,
    'passing the error information
    intAction = gobjErrorHandler.ErrorProcess(ModuleName, _
        SUBNAME, Err.Number, Err.Description)
    'Evaluate return value to determine what action to take
    Select Case intAction
        Case ERR_CONTINUE
            Resume Next
        Case ERR_RETRY
            Resume
        Case ERR_EXIT
            Resume GetCompanyInfo_Exit
        Case ERR_QUIT
            Quit
    End Select
End Sub

Notice the On Error Goto statement at the beginning of the routine and that the local constant SUBNAME is declared and set equal to GetCompanyInfo. The generic error handler uses the value in the constant to display the routine within which the error occurred. The error handler GetCompanyInfo_Err instantiates the ErrorHandler class. It executes the ErrorProcess method of the class and then evaluates its return value.

Summary

In this chapter, you learned the alternatives for handling errors in your Access applications. This chapter covered how you can use the error event to trap for application and Jet Engine errors in forms and reports. You also learned how to use the On Error statement. Finally, you learned how to build a generic error system. Regardless of the amount of testing done on an application, errors will occur. It is important that you properly trap for those errors.

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

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