Thursday, October 9, 2008

Easy Error-handling in Microsoft Visual Basic 6

I developed a technique to handle VB6 runtime errors a number of years ago and it still serves me well. I am offering it to anyone who might find it useful.

Summary

It works by raising any runtime error up the call stack to the top-level procedure—which displays a message box showing the error description and call stack information. The call stack is suppressed if the error is designated as a business error (see below).

There are three error-handling templates. (By template I mean generic code that can be pasted directly into your procedures.)

One template is for "top-level" procedures (a top-level procedure is usually an event procedure). This template's error handler displays a message box.

The second template is placed in procedures that are neither top-level nor located in classes. Its error handler simply raises an error to the procedure that called it, adding itself to the call stack.

The third template, intended for classes, is similar to the second template, but also manages the vbObjectError constant.

Message Box and Call Stack

Sample:



Note that the message box title is the application title as defined in the IDE's menu under Project > Properties > Make > Title and accessed programmatically by the construct App.Title.

In the call stack, the top-level procedure is listed first, and the procedure in which the error occurred is listed last. Intermediate procedures are listed in the proper order. Sample call stack:

Error stack:
Sales Order Import.frmOrderProperties.Form_Load
Sales Order Import.frmOrderProperties.LoadControls

In this example, the runtime error occurred in the LoadControls method and the message box was displayed by the error handler of the Form_Load subroutine.

Business Errors

The templates have a provision for what I call business errors—errors that convey user-friendly or non-technical information. They aren't necessarily errors in the sense that something is wrong; they can be informative. Thus, call stack information is suppressed. In programmatic terms, a business error is defined by a leading underscore in the error description. The top-level template simply discards the underscore before displaying the message box. To raise a business error, write code like this:

Err.Raise 512, , "_File is incorrect format."

I don't bother with custom business error numbers because of the possibility that the same number is shared by different pieces of software used by the application. Because Visual Basic reserves for itself error numbers 0 to 511, my business errors always have the number 512.

Module Declarations

In addition, I recommend adding module-level variables to hold error values. You can make them local or even global if you prefer. Also, each module and procedure has a local constant that identifies the procedure name. The error handlers use this information to build the call-stack string.

Paste these declarations into every form, class, and code module. (Of course, you should substitute the name of each particular module into the mModuleName constant.)

Private Const mModuleName               As String = "frmOrderProperties"

' Error variables.
Private mErrNo                          As Long
Private mErrDescr                       As String
Private mErrSource                      As String


Procedure Declarations

Define a constant to hold the procedure name. Also, be sure to add the On Error GoTo statement to make the error trap operational:

Const ProcedureName                 As String = "Form_Load"

On Error GoTo ErrorRtn

Notes on the Code

I use the ExitRtn label to enforce a single exit point from the procedure, which is handy if cleanup code is necessary (such code would go under the ExitRtn label). Thus, instead of calling Exit Sub I call GoTo ExitRtn. Another benefit is to avoid excessive nesting; instead of this:

If PKID <> "" Then
    ' Do stuff.
End If


I write this:

If PKID = "" Then
    GoTo ExitRtn
End If

' Do stuff.


The main logic (under the comment "Do stuff") is thus not nested inside the If . . . End If block and in the subsequent code the programmer is free to ignore the If . . . End If block, which reduces his mental clutter.

If you're wondering how I formatted the source code in this post, see "Formatting Visual Basic 6 code for HTML."

Top-Level Template

Paste this code into the bottom of your event procedures. I've shown the End Sub for completeness.

ExitRtn:
    Exit Sub
ErrorRtn:
    mErrNo = Err.Number
    mErrDescr = Err.Description
    mErrSource = Err.Source
    If Left$(mErrDescr, 1) = "_" Then
        ' Business rule violation.
        mErrDescr = Mid$(mErrDescr, 2)
        mErrSource = ""
    ElseIf Len(mErrSource) > 0 And LCase$(mErrSource) <> LCase$(App.EXEName) Then
        ' Runtime error occurred in another procedure.
        mErrSource = vbNewLine & vbNewLine & "Error stack: " & vbNewLine & App.Title & "." & mModuleName & "." & ProcedureName & vbNewLine & mErrSource
    Else
        ' Runtime error occurred in this procedure.
        mErrSource = vbNewLine & vbNewLine & "Error stack: " & vbNewLine & App.Title & "." & mModuleName & "." & ProcedureName
    End If
    MsgBox mErrDescr & mErrSource, vbExclamation, App.Title
    Resume ExitRtn
End Sub


Second Template

This code goes at the bottom of non-class procedures that are not at the top level. Normally, these would be non-event-handling procedures that are outside classes.

ExitRtn:
    Exit Function
ErrorRtn:
    mErrNo = Err.Number
    mErrDescr = Err.Description
    mErrSource = Err.Source
    If Left$(mErrDescr, 1) = "_" Then
        ' Business rule violation.
        mErrSource = ""
    ElseIf Len(mErrSource) > 0 And LCase$(mErrSource) <> LCase$(App.EXEName) Then
        ' Runtime error occurred in another procedure.
        mErrSource = App.Title & "." & mModuleName & "." & ProcedureName & vbNewLine & mErrSource
    Else
        ' Runtime error occurred in this procedure.
        mErrSource = App.Title & "." & mModuleName & "." & ProcedureName
    End If
    Err.Raise mErrNo, mErrSource, mErrDescr
End Function


Third Template

This code is for classes.

ExitRtn:
    Exit Sub
ErrorRtn:
    mErrNo = Err.Number
    mErrDescr = Err.Description
    mErrSource = Err.Source
    If Left$(mErrDescr, 1) = "_" Then
        ' Business rule violation.
        mErrSource = ""
    ElseIf ((mErrNo - vbObjectError < 0) Or (mErrNo - vbObjectError > 65535)) And (mErrNo >= vbObjectError) Then
        ' Error occurred in this procedure. Test for overflow error before adding constant.
        If mErrNo >= -262144 Then mErrNo = mErrNo + vbObjectError
        mErrSource = App.Title & "." & mModuleName & "." & ProcedureName
    Else
        ' Error occurred in another procedure.
        mErrSource = App.Title & "." & mModuleName & "." & ProcedureName & vbNewLine & mErrSource
    End If
    Err.Raise mErrNo, mErrSource, mErrDescr
End Sub


Update: this post has been re-published on my programming blog, Code Cache.

No comments:

Post a Comment