Practical Standards for Microsoft Visual Basic .NET (Pro-Developer)
[Previous] [ Next ]
When you run a project as a compiled program, untrapped errors are fatal ”they cause the program to terminate. You must make every effort to prevent this from happening. To prevent errors from stopping code execution (and terminating compiled programs), you create error handlers to trap the errors. When an error is trapped, Visual Basic doesn't display an error message or terminate the application. Instead, code that you've written to specifically handle the error is executed.
Every procedure should have an error handler, regardless of the amount of code it contains. It's best to place an On Error statement as the first line of code, immediately after the procedure header and just before the variable declarations. Be aware that errors can "bubble up" the call stack to error handlers in procedures higher in the stack (as I'll discuss later in this chapter). If a procedure's errors are allowed to bubble up in this manner, you should clearly explain this behavior in a prominent comment at the top of the procedure.
There are essentially two ways to trap errors:
- Divert code execution when an error occurs by using On Error GoTo.
- Ignore the error without interrupting or diverting code execution by using On Error Resume Next .
You can create multiple error handlers in a procedure, but no more than one error handler can be enabled at a time. Visual Basic treats the handler identified in the most recent On Error statement (discussed in the next section) as the enabled error handler. It's often advantageous to switch error handlers at different points within a procedure; understanding how the various error handlers work is crucial to taking advantage of this capability.
Ignoring Errors by Using On Error Resume Next
The simplest (and most dangerous) method of handling errors uses the On Error Resume Next statement. On Error Resume Next specifies that errors are completely ignored; the offending line of code is simply skipped and execution continues with the next statement.
For example, the following procedure has a run-time error ”in this case, a divide-by-zero error ”that is handled by the On Error Resume Next error handler:
PrivateSub cmdGenerateError_Click() '*Purpose:TestOnErrorResumeNext OnErrorResumeNext Debug.Print 10/0 EndSub |
The Debug.Print statement causes a divide-by-zero error. However, because there is an enabled error handler (specified by On Error Resume Next ), the error is ignored and execution resumes at the next statement (that is, the End Sub statement).
Just because an error is ignored doesn't mean you'll be unable to know that an error has occurred. After a statement causes an error, the Err object contains information about the error even though an error message isn't displayed. The following procedure illustrates how you can test the Err object at any time to determine whether an error has occurred. When this procedure is called, a message box is displayed with the text 11: Division by zero .
PrivateSub cmdGenerateError_Click() '*Purpose:DetectanerrorusingtheErrobject. OnErrorResumeNext Debug.Print 10/0 '*Ifanerroroccurred,displayitsnumberanddescription. If Err.Number>0 Then MsgBox Err.Number&":"&Err.Description,vbCritical EndIf EndSub |
This technique has many uses. For instance, say that you want to create a new record in a database using data entered in a text box and that the table in which you're creating the record does not allow duplicate values. You can try to locate a record matching the entry in the text box before saving it as a new record, or you can simply try to create the new record and test for Error 3022 (duplicate record), as shown in the following procedure:
PrivateSub CreateRecord() '*Purpose:Createarecordinadatabase '*fromthedataontheform. OnErrorResumeNext Dim rstMyTable As Recordset Const c_DuplicateRecordError=3022 '*CreateaRecordsetfromthemodule-levelDatabasevariable. Set rstMyTable=m_dbMyDatabase.OpenRecordset(_ "MyTable",dbOpenDynaset) '*Attempttoaddanewrecordtothedatabase. rstMyTable.AddNew rstMyTable![Name]=txtName.Text rstMyTable.Update '*Testforduplicatevalueerror. If Err.Number=c_DuplicateRecordError Then MsgBox "Thisnameexistsinthedatabase,andduplicates"&_ "arenotallowed.",vbOKOnly EndIf EndSub |
NOTE
On Error Resume Next is often useful when you work with objects. Checking the Err object after each interaction with an object allows you to know with certainty which object caused the error ”the object specified in Err.Source . This technique can require a considerable amount of code, however.
Diverting the Flow of Execution by Using On Error GoTo
Ignoring errors is very risky and is an inferior approach unless you're trapping an expected error, such as in the previous example. When an unexpected error occurs within a procedure, the procedure has problems. If you ignore the error, there might be serious repercussions for the user ”such as data not saving or saving incorrectly. More often than not, you'll need to take some course of action when an error occurs by redirecting code execution to an error handler specified in an On Error GoTo statement . This statement has the following syntax:
OnErrorGoTo line |
Note that line must refer to a statement in the same procedure as the On Error GoTo statement.
In this syntax, line has two possible meanings. The first is a literal line number to branch to when the error occurs. But the line number doesn't mean the physical position of the line in the procedure. Consider this next code example:
PrivateSub TestErrorHandler() '*Purpose:TesttheOnErrorGoTostatementbydeliberately '*generatingarun-timeerror. OnErrorGoTo 4 Debug.Print "Line2" Debug.Print 10/0 Debug.Print "Line4" Debug.Print "Line5" EndSub |
You might think that the division-by-zero error would cause code execution to continue at the statement that prints the text Line 4 because this is the fourth statement of code (not counting comments). Not only does this not happen, but this code actually causes a compile error and won't execute at all. When you designate a line number, you must label a specific statement with that line number, as shown here:
PrivateSub TestErrorHandler() '*Purpose:TesttheOnErrorGoTostatementbydeliberately '*generatingarun-timeerror. OnErrorGoTo 4 Debug.Print "Line2" Debug.Print 10/0 Debug.Print "Line4" 4 Debug.Print "Line5" EndSub |
Even though the line beginning with 4 is not actually the fourth line of code, it's the statement that executes immediately after the division-by-zero occurs. The actual numbers you use are inconsequential ”they need not correspond to physical line numbers or be in any sequential order.
NOTE
You cannot use line number 0. This has a special use, which I'll discuss later in this chapter.
The second and more common use of line is to supply a line label. A line label is a string of text that identifies a single line of code. Line labels can be any combination of characters that starts with a letter and ends with a colon (:). Labels are not case sensitive and must always appear at the start of the line. For example, the procedure below contains a label. The On Error GoTo statement diverts execution to the line labeled PROC_ERR when the division-by-zero error occurs.
PrivateSub TestAnErrorLabel() '*Purpose:Usealinelabeltodivertcodeexecution. OnErrorGoTo PROC_ERR Debug.Print "Line2" Debug.Print 10/0 Debug.Print "Line4" PROC_ERR: Debug.Print "Line6" EndSub |
NOTE
Labels should appear in all-uppercase letters for visual emphasis, as I'll discuss in Chapter 11, "Controlling Code Flow." Also, you should never put more than one statement of code on a single line ”see Chapter 8, "Formatting Code." This rule applies to labels as well, since they are considered code statements. When control jumps to a label, execution will simply continue with the line following the label.
The On Error GoTo statement is used primarily to divert code execution to an error handler. To ensure that an error handler doesn't execute unless an error occurs, you should precede it with an Exit Sub , Exit Function , or Exit Property statement. (If you follow Directive 3.2 in Chapter 3, "Designing Modules and Procedures," the Exit statement will immediately follow the PROC_EXIT label and will be the procedure's single exit point.) The following is a skeleton procedure with a blank error handler and a labeled exit point:
PrivateSub MyProcedure() '*Purpose: OnErrorGoTo PROC_ERR |
In this example, an error causes execution to jump to the PROC_ERR label, where it proceeds until it reaches the End Sub statement. You don't have to let the code run through the remainder of a procedure until it reaches an End Sub , End Function , or End Property statement. You can also do the following:
- Use an Exit Sub , Exit Function , or Exit Property statement to force code execution to leave the procedure. (Note that you should always use a single exit point rather than exiting the code directly from the error handler.)
- Use a GoTo PROC_EXIT statement to force execution to the procedure's single exit point.
- Use a Resume Next statement to force execution to continue with the line immediately following the statement that generated the error.
- Use a Resume statement to force execution to return to and continue executing the line that generated the error.
- Use a Resume <line> statement to redirect execution to a specific statement.
Using GoTo PROC_EXIT in combination with an Exit Sub , Exit Function , or Exit Property statement to force code execution to leave the procedure is straightforward:
PrivateSub MyProcedure() '*Purpose:TestthebehaviorofGoToinanerrorhandler. OnErrorGoTo PROC_ERR Debug.Print 10/0 PROC_EXIT: ExitSub PROC_ERR: MsgBox Me.Name&"MyProcedure"&vbCrLf&Err.Number&vbCrLf&_ Err.Description GoTo PROC_EXIT EndSub |
The following example shows how you can use the ResumeNext statement to force execution to continue with the line immediately following the statement that caused an error:
PrivateSub MyProcedure() '*Purpose:TestthebehaviorofResumeNextinan '*errorhandler. OnErrorGoTo PROC_ERR '*Errorisgeneratedonthenextline. Debug.Print 10/0 Debug.Print "CodeReturnsHere" PROC_EXIT: ExitSub PROC_ERR: MsgBox Me.Name&"MyProcedure"&vbCrLf&Err.Number&vbCrLf&_ Err.Description ResumeNext EndSub |
The error in this procedure causes execution to jump to the code within the PROC_ERR block. When Resume Next is encountered , execution continues with the statement immediately following the statement that caused the error.
You can branch to a specific code statement within the procedure in which the error occurs by using Resume <line>, as shown below. The line parameter works just like it does in the On Error Resume <line> statement.
PrivateSub MyProcedure() '*Purpose:DemonstrateResume<line>inanerrorhandler. OnErrorGoTo PROC_ERR Debug.Print "Line1" Debug.Print 10/0 Debug.Print "Line3" ERR_CONTINUE: Debug.Print "Line5" PROC_EXIT: ExitSub PROC_ERR: Resume ERR_CONTINUE EndSub |
Perhaps the best thing an error handler can do is fix the error. If it can fix the error and return control to the offending statement, it's almost as if the error never occurred. Although the method used to remedy an error depends on the situation, returning to the offending line is simple and consistent. To force execution to return to the statement that caused the error, use the Resume statement, as shown here:
PrivateSub cmdTestErrorHandler_Click() '*Purpose:DemonstratetheResumestatementinan '*errorhandler. OnErrorGoTo PROC_ERR Dim intFileNumber AsInteger Dim intRetries AsInteger Const c_MaxRetries=10 Const c_LockedFileError=70 intFileNumber=1 Open "C:\Test.txt" ForOutputAs #intFileNumber PROC_EXIT: ExitSub PROC_ERR: '*Iftheerrorindicatesthatthefileislocked,attempttoopen '*thefileagainuntilc_MaxRetriesattemptshavebeenmade. If Err.Number=c_LockedFileError Then '*Ifthemaximumnumberofattemptshasbeenmade,tellthe '*userthatthefileislockedandgetout. If intRetries>c_MaxRetries Then MsgBox"Fileislocked!",vbExclamation GoTo PROC_EXIT EndIf '*CallacustomPauseprocedurethatpausesaspecified '*numberofmilliseconds. Call Pause(100) intRetries=intRetries+1 Resume EndIf EndSub |
This example attempts to open a file called C:\Test.txt. If the file is locked by another application, an error occurs and execution is sent to the error handler. The error handler then does the following:
- Checks to see whether the error is a locking error.
- If the error is a locking error, it checks intRetries to determine how many attempts have been made to open the file.
- If the error handler has been invoked fewer than the maximum times as specified by c_MaxRetries , it calls a custom Pause routine. Then intRetries is incremented and execution returns to the Open statement.
- If the error handler has been invoked more than the allowable number of times, a message is displayed to the user and the procedure is exited.
Error Handlers and the Call Stack
It's extremely important to understand how errors are passed up the call stack. A couple of terms are critical to such an understanding. The enabled error handler is the error handler most recently specified in an On Error statement. An active error handler is an error handler that is in the process of handling an error. Note that it is possible for a handler to be enabled but not active. Once an error handler is enabled, it remains enabled until another error handler is enabled or the procedure containing the error handler goes out of scope. When a procedure containing an enabled error handler goes out of scope, execution returns to the calling procedure and the last enabled error handler becomes enabled again.
Consider the following two procedures:
PrivateSub cmdCreateErrorHandler_Click() '*Purpose:Enableanerrorhandlerandcallanotherprocedure. OnErrorResumeNext Call TestSub EndSub PrivateSub TestSub() '*Purpose:Demonstrateerrorhandlersandthecallstack. OnErrorGoTo PROC_ERR Debug.Print 10/0 PROC_EXIT: ExitSub PROC_ERR: GoTo PROC_EXIT EndSub |
When the cmdCreateErrorHandler button is clicked, the On Error Resume Next statement enables an error handler. When the TestSub procedure is invoked, its error handler becomes enabled; any errors encountered within the TestSub procedure (such as the divide-by-zero error) are handled by the PROC_ERR error handler. When the TestSub procedure completes and code execution returns to the Click event of the command button, the Click event's error handler becomes enabled once more.
Next consider these two procedures:
PrivateSub cmdCreateErrorHandler_Click() '*Purpose:Enableanerrorhandlerandcallanotherprocedure. OnErrorResumeNext Call TestSub EndSub PrivateSub TestSub() |
When the command button is clicked, the On Error Resume Next statement within the Click event enables an error handler. When the TestSub procedure is invoked, error handlers in that procedure have the opportunity to become enabled. Since the TestSub procedure contains no error handler, the most recently enabled error handler (from the Click event) remains enabled throughout execution of the TestSub procedure.
What happens when an error is encountered within the TestSub procedure? The following two procedures illustrate this situation:
PrivateSub cmdCreateErrorHandler_Click() '*Purpose:Demonstrateerrorhandlersandthecallstack. OnErrorResumeNext Call TestSub MsgBox "Statementinfirstprocedure",vbInformation EndSub PrivateSub TestSub() '*Generatearun-timeerror. Debug.Print 10/0 MsgBox "Statementaftererror",vbInformation EndSub |
When the Click event is fired , an error handler is enabled using On Error Resume Next . When execution transfers to the TestSub procedure, the error handler remains enabled because no error handler is enabled within the TestSub procedure. When the division-by-zero error occurs, what's printed? Since there is no error handler in the TestSub procedure, execution returns immediately to the calling procedure (the cmdCreateErrorHandler_Click procedure) to be handled by its enabled error handler. Since the error handler is a Resume Next error handler, you might think that the statement immediately following the error (in the TestSub procedure) would be the next to be executed. It isn't. Instead, the statement immediately following the call to TestSub becomes the next statement, and the text "Statement in first procedure" is printed. An On Error statement cannot direct code execution outside of the procedure in which it exists.
This concept is true for multiple nested procedures as well. If an error occurs within a procedure that doesn't have an enabled error handler, the procedure that called the procedure with the error is checked for an enabled, but not active, error handler. This continues up the call stack until an enabled, but not active, error handler is found or the top of the call stack is reached. If the top of the call stack is reached, the error is treated as an untrapped error (and rightly so). If an enabled, but not active, error handler is encountered, it is executed, and then execution continues in the procedure that contains that handler.
Disabling Error Handlers at Run Time by Using On Error GoTo 0
Sometimes you might need to disable an enabled error handler at run time. Earlier in this chapter you learned that you can't use line number 0 when using On Error GoTo. This is because using line number 0 disables the currently enabled error handler.
Consider this procedure:
PrivateSub cmdDisableErrorHandler_Click() '*Purpose:Demonstratedisablinganerrorhandler '*atruntime. OnErrorGoTo PROC_ERR OnErrorGoTo Debug.Print 10/0 PROC_EXIT: ExitSub PROC_ERR: Call ShowError(Me.Name,"cmdDisableErrorHandler",Err.Number,_ Err.Description) GoTo PROC_EXIT EndSub |
The first statement in this procedure enables an error handler. However, the On Error GoTo 0 statement disables the error handler. Consequently, the division-by-zero error is not trapped and Visual Basic displays an error message.
NOTE
On Error GoTo 0 disables the enabled error handler only in the current procedure. If an error is encountered after an On Error GoTo 0 statement, the error is passed up the call stack as if there were no error handler in the procedure at all. If an enabled, but not active, error handler is found higher in the call stack, that error handler handles the error.
Enabling and Disabling Error Handlers in Debug Mode
Although you don't want errors to go untrapped in a compiled program, it's often advantageous when running a program in the IDE to let Visual Basic halt code execution if an error occurs. When execution halts, you receive a relevant error message and are shown the offending line of code; this greatly aids the debugging process. The approach Visual Basic takes to handling errors encountered at design time is determined by the Error Trapping property of the Visual Basic IDE. You set this property in the Options dialog box, shown in Figure 7-2.
The Error Trapping property is a property of the Visual Basic environment, not of a specific project. Each project you work with ”even after you shut down and restart Visual Basic ”uses this setting. To set the error trapping option for the current session of Visual Basic only without changing the default for future sessions, use the Toggle command on the code window's shortcut menu. (See Figure 7-3.)
You can set the Error Trapping property to one of the following values:
- Break On All Errors
- Break In Class Module
- Break On Unhandled Errors
Break On All Errors essentially disables all of your error handlers. When an error occurs, regardless of whether an error handler is enabled, the code enters break mode at the offending statement and Visual Basic shows an error message. This allows you to deal with unexpected errors while testing within the IDE.
The Break In Class Module setting is most useful when you debug ActiveX components . Ordinarily, the enabled error handler within a procedure that calls a method of an ActiveX component handles any errors that are not handled within the ActiveX component's procedure. The Break In Class Module setting specifies that errors not handled within the ActiveX component will cause the ActiveX project to enter break mode at the statement that caused the error. This prevents errors not handled within the ActiveX component from being passed up the call stack to the procedure in the client program, thus making it considerably easier to debug the ActiveX component.
The Break On Unhandled Errors setting closely models how errors are treated in compiled programs. Errors trapped by enabled error handlers are dealt with by those error handlers, and only unhandled errors cause the program to enter break mode.