VBA Excel Error Handling - especially in functions

2019-03-13 08:05发布

问题:

I got the book "Professional Excel Development" by Rob Bovey and it is opening up my eyes.

I am refitting my code with error handling. However, there is a lot I don't understand. I especially need to know how to correctly use it in functions. I use Bovey's rethrow version of the error handler (at bottom). When I started, I was using the basic boolean (non-rethrow) method and turned my subroutines into boolean functions. (P.S. I am switching back to the boolean method based on the answer.)

I need guidance on how to fit functions into this scheme. I want them to return their real values (a string or double, e.g., or -1 if they fail in some cases) so I can nest them in other functions and not just return an error handling boolean.

This is what a typical subroutine call to bDrawCellBorders(myWS) would look like within an entry point. Sub calls seem to be working well. (I.e. it is a subroutine that was turned into a function only so it can return a boolean to the error handling scheme.)

Sub UpdateMe()  ' Entry Point

    Const sSOURCE As String = "UpdateMe()"

    On Error GoTo ErrorHandler

    Set myWS = ActiveCell.Worksheet
    Set myRange = ActiveCell
    myWS.Unprotect

' lots of code

    If Not bDrawCellBorders(myWS) Then ERR.Raise glHANDLED_ERROR    ' Call subroutine

' lots of code

ErrorExit:
    On Error Resume Next
    Application.EnableEvents = True
    myWS.Protect AllowFormattingColumns:=True
    Exit Sub

ErrorHandler:
    If bCentralErrorHandler(msMODULE, sSOURCE,,True) Then  ' Call as Entry Point
        Stop
        Resume
    Else
        Resume ErrorExit
    End If
End Sub

However, I don't know how to extend this to real functions. This is based off an example in the book that was drawn up for a subroutine, and I just switched it to a function. Questions: * How do I call it? Is it simply like x = sngDoSomeMath(17) * Will its error handling function properly? * Where is the right place or places to call the error handling routine with bReThrow=true?

Public Function sngDoSomeMath(ByVal iNum As Integer) As Single

Dim sngResult As Single

Const sSOURCE As String = "sngDoSomeMath()"

On Error GoTo ErrorHandler

' example 1, input did not pass validation. don't want to 
' go up the error stack but just inform the
' calling program that they didn't get a good result from this 
' function call so they can do something else
If iNum <> 42 Then
    sngResult = -1    'function failed because I only like the number 42
    GoTo ExitHere
End If

' example 2, true error generated
sngResult = iNum / 0

sngDoSomeMath = lResult

ExitHere:
    Exit Function
ErrorHandler:

' Run cleanup code
'  ... here if any

' Then do error handling

If bCentralErrorHandler(msMODULE, sSOURCE, , , True) Then ' The true is for RETHROW
    Stop
    Resume
End If

End Function

The Error Handler Routine:

'
' Description:  This module contains the central error
'               handler and related constant declarations.
'
' Authors:      Rob Bovey, www.appspro.com
'               Stephen Bullen, www.oaltd.co.uk
'
' Chapter Change Overview
' Ch#   Comment
' --------------------------------------------------------------
' 15    Initial version
'
Option Explicit
Option Private Module

' **************************************************************
' Global Constant Declarations Follow
' **************************************************************
Public Const gbDEBUG_MODE As Boolean = False    ' True enables debug mode, False disables it.
Public Const glHANDLED_ERROR As Long = 9999     ' Run-time error number for our custom errors.
Public Const glUSER_CANCEL As Long = 18         ' The error number generated when the user cancels program execution.


' **************************************************************
' Module Constant Declarations Follow
' **************************************************************
Private Const msSILENT_ERROR As String = "UserCancel"   ' Used by the central error handler to bail out silently on user cancel.
Private Const msFILE_ERROR_LOG As String = "Error.log"  ' The name of the file where error messages will be logged to.


''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Comments: This is the central error handling procedure for the
'           program. It logs and displays any run-time errors
'           that occur during program execution.
'
' Arguments:    sModule         The module in which the error occured.
'               sProc           The procedure in which the error occured.
'               sFile           (Optional) For multiple-workbook
'                               projects this is the name of the
'                               workbook in which the error occured.
'               bEntryPoint     (Optional) True if this call is
'                               being made from an entry point
'                               procedure. If so, an error message
'                               will be displayed to the user.
'
' Returns:      Boolean         True if the program is in debug
'                               mode, False if it is not.
'
' Date          Developer       Chap    Action
' --------------------------------------------------------------
' 03/30/08      Rob Bovey       Ch15    Initial version
'
Public Function bCentralErrorHandler( _
       ByVal sModule As String, _
       ByVal sProc As String, _
       Optional ByVal sFile As String, _
       Optional ByVal bEntryPoint As Boolean, _
       Optional ByVal bReThrow As Boolean = True) As Boolean

    Static sErrMsg As String

    Dim iFile As Integer
    Dim lErrNum As Long
    Dim sFullSource As String
    Dim sPath As String
    Dim sLogText As String

    ' Grab the error info before it's cleared by
    ' On Error Resume Next below.
    lErrNum = ERR.Number
    ' If this is a user cancel, set the silent error flag
    ' message. This will cause the error to be ignored.
    If lErrNum = glUSER_CANCEL Then sErrMsg = msSILENT_ERROR
    ' If this is the originating error, the static error
    ' message variable will be empty. In that case, store
    ' the originating error message in the static variable.
    If Len(sErrMsg) = 0 Then sErrMsg = ERR.Description

    ' We cannot allow errors in the central error handler.
    On Error Resume Next

    ' Load the default filename if required.
    If Len(sFile) = 0 Then sFile = ThisWorkbook.Name

    ' Get the application directory.
    sPath = ThisWorkbook.Path
    If Right$(sPath, 1) <> "\" Then sPath = sPath & "\"

    ' Construct the fully-qualified error source name.
    sFullSource = "[" & sFile & "]" & sModule & "." & sProc

    ' Create the error text to be logged.
    sLogText = "  " & sFullSource & ", Error " & _
               CStr(lErrNum) & ": " & sErrMsg

    ' Open the log file, write out the error information and
    ' close the log file.
    iFile = FreeFile()
    Open sPath & msFILE_ERROR_LOG For Append As #iFile
    Print #iFile, Format$(Now(), "mm/dd/yy hh:mm:ss"); sLogText
    If bEntryPoint Or Not bReThrow Then Print #iFile,
    Close #iFile

    ' Do not display or debug silent errors.
    If sErrMsg <> msSILENT_ERROR Then

        ' Show the error message when we reach the entry point
        ' procedure or immediately if we are in debug mode.
        If bEntryPoint Or gbDEBUG_MODE Then
            Application.ScreenUpdating = True
            MsgBox sErrMsg, vbCritical, gsAPP_NAME
            ' Clear the static error message variable once
            ' we've reached the entry point so that we're ready
            ' to handle the next error.
            sErrMsg = vbNullString
        End If

        ' The return vale is the debug mode status.
        bCentralErrorHandler = gbDEBUG_MODE

    Else
        ' If this is a silent error, clear the static error
        ' message variable when we reach the entry point.
        If bEntryPoint Then sErrMsg = vbNullString
        bCentralErrorHandler = False
    End If

    'If we're using re-throw error handling,
    'this is not the entry point and we're not debugging,
    're-raise the error, to be caught in the next procedure
    'up the call stack.
    'Procedures that handle their own errors can call the
    'central error handler with bReThrow = False to log the
    'error, but not re-raise it.
    If bReThrow Then
        If Not bEntryPoint And Not gbDEBUG_MODE Then
            On Error GoTo 0
            ERR.Raise lErrNum, sFullSource, sErrMsg
        End If
    Else
        'Error is being logged and handled,
        'so clear the static error message variable
        sErrMsg = vbNullString
    End If

End Function

回答1:

That is an amazing book by Rob.

My two cents of Error Handling (Either for a procedure or a Function) is based on KISS (Keep it simple Silly)

Understand what do you want from your error handler?

This is usually what I want/expect from my error handler...

  1. Line on which the error happened
  2. Error Number
  3. Error Message
  4. Reset Events if applicable

Lets break the above. As you are by now already aware how your error handler looks like, Consider this example.

Sub Sample()
    Dim i As Integer, j As Integer

    On Error GoTo Whoa

    Application.ScreenUpdating = False

    i = 1111111111

    For j = 1 To i
        Debug.Print ThisWorkbook.Sheets(1).Cells(i, 1).Value
    Next i

LetsContinue:
    Exit Sub
Whoa:
    MsgBox Err.Description
    Resume LetsContinue
End Sub

This is a very basic error handler but it's of very less help to me. So let's now tweak it to make it more useful. If you run the above code you get an error message like shown in the screenshot below and if you notice, it's not of much help.

Let's now tackle all the points that I mentioned in the Logic above

  1. Line on which the error happened

There is a property called ERL which very few people are aware of. You can actually use it to get the line number of the code where the error happened. For that you have to ensure you number your code. See this example.

Sub Sample()
    Dim i As Integer, j As Integer

10      On Error GoTo Whoa

20      Application.ScreenUpdating = False

30      i = 1111111111

40      For j = 1 To i
50          Debug.Print ThisWorkbook.Sheets(1).Cells(i, 1).Value
60      Next j

LetsContinue:
70      Exit Sub
Whoa:
80      MsgBox Erl
90      Resume LetsContinue
End Sub

When you run the above code, you will get this

So now I know that the error happened on Line 30 which is i = 1111111111

Moving on to next

  1. Error Number
  2. Error Message

The error number and the error message can be retrieved from Err.Number and Err.Description respectively. So now let's combine Erl, Err.Number and Err.Description

Check this example

Sub Sample()
    Dim i As Integer, j As Integer

10      On Error GoTo Whoa

20      Application.ScreenUpdating = False

30      i = 1111111111

40      For j = 1 To i
50          Debug.Print ThisWorkbook.Sheets(1).Cells(i, 1).Value
60      Next j

LetsContinue:
70      Exit Sub
Whoa:
80      MsgBox "The Error Happened on Line : " & Erl & vbNewLine & _
           "Error Message : " & Err.Description & vbNewLine & _
           "Error Number : " & Err.Number
90      Resume LetsContinue
End Sub

When you run this code, you will get something like this.

You can choose to further customize the Error Message to make it more user friendly. For example

'~~> Message you want to deliver to the user in case the error happens
Const sMsg As String = "Please take a screenshot of this message and contact the developer for a resolution"
'~~> Title of your message box
Const sTitle As String = "Oopsie Daisies"

'~~> Change the above as applicable

Sub Sample()
    Dim i As Integer, j As Integer

10      On Error GoTo Whoa

20      Application.ScreenUpdating = False

30      i = 1111111111

40      For j = 1 To i
50          Debug.Print ThisWorkbook.Sheets(1).Cells(i, 1).Value
60      Next j

LetsContinue:
70      Exit Sub
Whoa:
80      MsgBox "The Error Happened on Line : " & Erl & vbNewLine & _
           "Error Message : " & Err.Description & vbNewLine & _
           "Error Number : " & Err.Number & vbNewLine & vbNewLine & _
           sMsg, vbCritical, sTitle
90      Resume LetsContinue
End Sub

On to the next one :)

Reset Events if applicable

When you are working with events and an error occurs, if there is no error handling, the code breaks. Unfortunately that doesn't reset the events. It is very important that you reset the events in the Error handler.

If you notice in the above code we are setting the Application.ScreenUpdating = False. When the code breaks, that event doesn't get reset. You will have to handle that in the Error handler LetsContinue in this case. See this example.

'~~> Message you want to deliver to the user in case the error happens
Const sMsg As String = "Please take a screenshot of this message and contact the developer for a resolution"
'~~> Title of your message box
Const sTitle As String = "Oopsie Daisies"

'~~> Change the above as applicable

Sub Sample()
    Dim i As Integer, j As Integer

10      On Error GoTo Whoa

20      Application.ScreenUpdating = False

30      i = 1111111111

40      For j = 1 To i
50          Debug.Print ThisWorkbook.Sheets(1).Cells(i, 1).Value
60      Next j

LetsContinue:
70      Application.ScreenUpdating = True
80      Exit Sub
Whoa:
90      MsgBox "The Error Happened on Line : " & Erl & vbNewLine & _
           "Error Message : " & Err.Description & vbNewLine & _
           "Error Number : " & Err.Number & vbNewLine & vbNewLine & _
           sMsg, vbCritical, sTitle
100     Resume LetsContinue
End Sub

Like Philippe, I also strongly suggest that you use MZ-Tools for VBA. I have been using it now for donkey years...

Hope this helps.



回答2:

I needed a bit more help on this specific technique so I went right to the source and Mr. Bovey was gracious enough to reply. He gave me permission to post his response to the StackOverflow community.

The instructions below refer to his preferred method of error handling for functions the "boolean error handling" technique and not to the alternate "rethrow method", both described in his book "Professional Excel Development" 2nd edition.


Hi Shari,

In answer to your questions about error handling in functions, there are three error handling scenarios you can have with a function in VBA:

1) The function is so trivial that is doesn't need an error handler. In the unlikely event an error occurs in a function like this it will spill over into the error handler of the calling procedure.

2) A non-trivial function needs an error handler and uses the Boolean return value system described in the book. Any other values the function needs to return are returned through ByRef arguments. This case covers the vast majority of functions I write. There are some things you can't do with functions like this, feeding them directly into the argument of another function is one example, but I consider this a good tradeoff in order to achieve bullet proof error handling.

3) A non-trivial function needs an error handler and must return a value not related to its error status. This is a rare situation because I can convert 99% plus of these into case 2 by restructuring my code. If you can't do this, your only choice is to select an arbitrary return value that is out of the range of normal return values and use this to indicate that an error has occurred. If the caller of this function sees this arbitrary error flag value it knows it can't continue.

Rob Bovey Application Professionals http://www.appspro.com/


Code Example (Shari W)


' Show how to call a function using this error handling method.
Const giBAD_RESULT As Integer = -1

Function TestMath()   ' An Entry Point

    Dim sngResult As Single
    Dim iNum As Integer

    ' Call the function, actual result goes in sngResult but it returns the error handling boolean.
    ' A true error like Div 0 will go to error handler.

    ' Set Up Error Handling for Entry Point
    Application.EnableCancelKey = xlErrorHandler
    Dim bUserCancel As Boolean
    Const sSOURCE As String = "TestMath()"
    On Error GoTo ErrorHandler
    ' End Error Set Up

    iNum = 0   ' Try 0 to create error
    If Not bDoSomeMath(iNum, sngResult) Then ERR.Raise glHANDLED_ERROR
    ' If function does parameter checking and wants to return a bad input code, check for that.
    If sngResult = giBAD_RESULT Then
        MsgBox ("Bad input to bDoSomeMath " & iNum)
    Else
        MsgBox ("I believe the answer is " & sngResult)
    End If

ErrorExit:
    On Error Resume Next
    Exit Function

ErrorHandler:
    If bCentralErrorHandler(msMODULE, sSOURCE, , True) Then
        Stop
        Resume
    Else
        Resume ErrorExit
    End If
End Function
Function bDoSomeMath(ByVal iNum As Integer, ByRef sngResult As Single) As Boolean

    ' Error handling Set Up
    Dim bReturn As Boolean
    Const sSOURCE As String = "bDoSomeMath()"
    On Error GoTo ErrorHandler
    bReturn = True
    ' End Error Set Up

    If iNum < 0 Or iNum > 1000 Then
        sngResult = giBAD_RESULT   'function failed because I only like the numbers 0 to 1000
        GoTo ErrorExit
    Else
        sngResult = 100 / iNum   ' generate a true error by iNum = 0
    End If

ErrorExit:
    On Error Resume Next
    bDoSomeMath = bReturn
    Exit Function

ErrorHandler:
    bReturn = False
    If bCentralErrorHandler(msMODULE, sSOURCE, , , True) Then
        Stop
        Resume
    Else
        Resume ErrorExit
    End If

End Function


回答3:

a proposal for error handling management in VBA can be found here .

The very same tool (MZ-Tools) and method (standard/generic error handler, which could be used to build an automated error reporting system) will work with Excel.