81

What are some good patterns for error handling in VBA?

In particular, what should I do in this situation:

... some code ...
... some code where an error might occur ...
... some code ...
... some other code where a different error might occur ...
... some other code ...
... some code that must always be run (like a finally block) ...

I want to handle both errors, and resume execution after the code where the error may occur. Also, the finally code at the end must always run - no matter what exceptions are thrown earlier. How can I achieve this outcome?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
jwoolard
  • 6,024
  • 9
  • 37
  • 37

13 Answers13

105

Error Handling in VBA

  • On Error Goto ErrorHandlerLabel
  • Resume (Next | ErrorHandlerLabel)
  • On Error Goto 0 (disables current error handler)
  • Err object

The Err object's properties are normally reset to a zero or a zero-length string in the error handling routine, but it can also be done explicitly with Err.Clear.

Errors in the error handling routine are terminating.

The range 513-65535 is available for user errors. For custom class errors, you add vbObjectError to the error number. See the Microsoft documentation about Err.Raise and the list of error numbers.

For not implemented interface members in a derived class, you should use the constant E_NOTIMPL = &H80004001.


Option Explicit

Sub HandleError()
  Dim a As Integer
  On Error GoTo errMyErrorHandler
    a = 7 / 0
  On Error GoTo 0
  
  Debug.Print "This line won't be executed."
  
DoCleanUp:
  a = 0
Exit Sub
errMyErrorHandler:
  MsgBox Err.Description, _
    vbExclamation + vbOKCancel, _
    "Error: " & CStr(Err.Number)
Resume DoCleanUp
End Sub

Sub RaiseAndHandleError()
  On Error GoTo errMyErrorHandler
    ' The range 513-65535 is available for user errors.
    ' For class errors, you add vbObjectError to the error number.
    Err.Raise vbObjectError + 513, "Module1::Test()", "My custom error."
  On Error GoTo 0
  
  Debug.Print "This line will be executed."

Exit Sub
errMyErrorHandler:
  MsgBox Err.Description, _
    vbExclamation + vbOKCancel, _
    "Error: " & CStr(Err.Number)
  Err.Clear
Resume Next
End Sub

Sub FailInErrorHandler()
  Dim a As Integer
  On Error GoTo errMyErrorHandler
    a = 7 / 0
  On Error GoTo 0
  
  Debug.Print "This line won't be executed."
  
DoCleanUp:
  a = 0
Exit Sub
errMyErrorHandler:
  a = 7 / 0 ' <== Terminating error!
  MsgBox Err.Description, _
    vbExclamation + vbOKCancel, _
    "Error: " & CStr(Err.Number)
Resume DoCleanUp
End Sub

Sub DontDoThis()
  
  ' Any error will go unnoticed!
  On Error Resume Next
  ' Some complex code that fails here.
End Sub

Sub DoThisIfYouMust()
  
  On Error Resume Next
  ' Some code that can fail but you don't care.
  On Error GoTo 0
  
  ' More code here
End Sub
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
guillermooo
  • 7,915
  • 15
  • 55
  • 58
  • 1
    this is great, but is there a place where all errors are listed So that I can know if mine is preexisting or if I need to create it? – PsychoData Feb 14 '14 at 18:38
  • 3
    @PsychoData, here is a list of the error codes http://support.microsoft.com/kb/146864 – Elias Feb 24 '14 at 17:33
  • How the code above should be changed in order to log on the enter and **every** exit of the called code (procedure, function, method and so on)? – Aleksey F. Jun 06 '15 at 04:12
41

I would also add:

  • The global Err object is the closest you have to an exception object
  • You can effectively "throw an exception" with Err.Raise

And just for fun:

  • On Error Resume Next is the devil incarnate and to be avoided, as it silently hides errors
Joel Goodwin
  • 5,026
  • 27
  • 30
  • 12
    +1 for warning about On Eror Resume Next. Probably one of the number one reasons why VB programs are generally so full of bugs. – Makis Jun 24 '09 at 12:50
  • 21
    Not true. When used correctly On Error Resume Next is an equivalent of try/catch. Correct use just requires checking or saving the error status after _every line_. It does make complex error checking much less verbose. HOWEVER, incorrectly used, all the above applies. – Ben McIntyre Oct 30 '13 at 06:45
  • 4
    I think everyone would agree that On Error is the equivalent of Try/Catch yes... but On Error Resume Next? It causes all errors to disappear - including the ones we never anticipated. It's better to let errors bleed out than head-scratch for weeks on why something weird is going on [this has happened to me when debugging someone else's code]. I only use it in very special circumstances, tight small functions where an oddity forces you into an error (e.g. does this item exist in a Collection). – Joel Goodwin Oct 31 '13 at 08:48
  • 3
    If you put too much code in errMyErrorHandler: you risk an error happening in your error handler which creates an endless loop. If you put On Error Resume Next before you process the error in errMyErrorHandler it resets the Err object and you lose the error information. I move my error processing to a sub and pass the err.num and description as parameters so I can then use On Error Resume Next as I reset everything like screenupdating and cursor etc and show the error using the param values... `Call mdl_val.usr_sub_handle_error(Err.Source, Err.Description)` – DWiener May 26 '14 at 00:15
  • 3
    "to be avoided" is not exactly. There are many cases requiring `On Error Resume Next`. The common principle of these cases is when some results are returned by throwing an exception. The most often case is the accessing of a `Collection` object by the string key: in this case the caller can't know if there is an item with that key in the `Collection` object. – Aleksey F. Jun 06 '15 at 04:06
  • 1
    @AlekseyF. yep, I did concede this in one of the comments above – Joel Goodwin Jun 22 '15 at 09:42
  • @Makis I know this is 10 years old... but you really should delete your comment... or at least change it. You are just wrong. Ben is right. – Excel Hero Mar 04 '19 at 18:11
  • I wrote a custom function to make it easier to ["Throw" errors in VBA](https://nolongerset.com/throwing-errors-in-vba/). It's an especially powerful technique when paired with [vbWatchdog](https://nolongerset.com/error-handling-evolution/). – mwolfe02 Jan 20 '21 at 17:28
  • "it silently hides errors" - Awesome! Extremely useful feature. Use it wisely. – johny why Sep 25 '21 at 03:00
20

So you could do something like this:

Function Errorthingy(pParam)
On Error GoTo HandleErr

    ' Your code here

    ExitHere:
    ' Your finally code
    Exit Function

    HandleErr:
        Select Case Err.Number
        ' Different error handling here'
        Case Else
            MsgBox "Error " & Err.Number & ": " & Err.Description, vbCritical, "ErrorThingy"
        End Select


    Resume ExitHere

End Function

If you want to bake in custom exceptions (e.g., ones that violate business rules), use the example above, but use the goto to alter the flow of the method as necessary.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Johnno Nolan
  • 29,228
  • 19
  • 111
  • 160
  • 2
    That's pretty much how we handled errors in a big VB6 application back in the days. Worked relatively well and was easy to use. IIRC, we had an error handling class that was called instead of having the error code in the function. That way it was much easier to change the behaviour as well. – Makis Jun 24 '09 at 12:52
  • It's generally a good idea to put "On Error GoTo 0" after the block of code where you need error handling. Besides, any error in the error handling code is terminating. – guillermooo Jun 25 '09 at 07:50
  • 5
    No idea if this is idiomatic VBA, but for .NET devs, if you rename "HandleErr" to "Catch" and "ExitHere" to "Finally" and squint... – user1454265 May 11 '15 at 18:10
  • 1
    @user1454265 ... then you can easily miss the `Resume ExitHere` which makes a lot of the difference between the two paradigms. – AntoineL Jul 27 '17 at 15:19
14

Here's my standard implementation. I like the labels to be self-descriptive.

Public Sub DoSomething()

    On Error GoTo Catch ' Try
    ' normal code here

    Exit Sub
Catch:

    'error code: you can get the specific error by checking Err.Number

End Sub

Or, with a Finally block:

Public Sub DoSomething()

    On Error GoTo Catch ' Try

    ' normal code here

    GoTo Finally
Catch:

    'error code

Finally:

    'cleanup code

End Sub
LimaNightHawk
  • 6,613
  • 3
  • 41
  • 60
  • 1
    What does happen if an exception is raised after `Finally:`? So `On Error GoTo 0` immediately after `Finally:` maybe needed to fix the unwanted recursion. – Aleksey F. Jun 07 '15 at 04:18
  • 2
    If there is an error after the `Finally` block, it will just throw the error. It will not re-loop back to the `Finally` block. (Try it, you'll see.) If you want to handle an error after the Finally block, you'll need to add another `On Error GoTo`, but probably with another label, like `Catch2`. But here we start to digress into Clean Code methodology --> a clean method will only need one error handler (and should even have it's own dedicated method for error catching.) – LimaNightHawk Jan 04 '17 at 14:21
  • 1
    @LimaNightHawk: I believe what happens after `Finally:` depends whether you enter it after having been diverted to `Catch:` (then yes it just throws out)... or *not*! And in that latter case, i.e. having gone through `GoTo Finally` will have the `On Error GoTo Catch` still in effect, so the control is diverted toward `Catch:` (could be a good thing), then `Finally:` is re-entered, probably not what you expected in the first place. – AntoineL Jul 27 '17 at 15:32
  • If you add another `On Error GoTo Catch2` in `Finally:` code, it will been effective in that latter case but **not** if you went through `Catch:` before, because there is no `On Error GoTo -1` nor any `Resume`; adding the former is bringing us so far away from regular `try catch finally` that one might consider stopping dubious analogy before that point. – AntoineL Jul 27 '17 at 15:32
  • @AntoineL Yes! Agree with both, great observation and clarification. – LimaNightHawk Jul 28 '17 at 16:15
8

Professional Excel Development (PED) has a pretty good error handling scheme. If you're going to spend any time in VBA, it's probably worth getting the book. There are a number of areas where VBA is lacking and this book has good suggestions for managing those areas.

PED describes two error handling methods. The main one is a system where all entry point procedures are subprocedures and all other procedures are functions that return Booleans.

The entry point procedure use On Error statements to capture errors pretty much as designed. The non-entry point procedures return True if there were no errors and False if there were errors. Non-entry point procedures also use On Error.

Both types of procedures use a central error handling procedure to keep the error in state and to log the error.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Dick Kusleika
  • 32,673
  • 4
  • 52
  • 73
5

Also relevant to the discussion is the relatively unknown Erl function. If you have numeric labels within your code procedure, e.g.,

Sub AAA()
On Error Goto ErrorHandler

1000:
' code
1100:
' more code
1200:
' even more code that causes an error
1300:
' yet more code
9999: ' end of main part of procedure
ErrorHandler:
If Err.Number <> 0 Then
   Debug.Print "Error: " + CStr(Err.Number), Err.Descrption, _
      "Last Successful Line: " + CStr(Erl)
End If
End Sub

The Erl function returns the most recently encountered numberic line label. In the example above, if a run-time error occurs after label 1200: but before 1300:, the Erl function will return 1200, since that is most recently successfully encountered line label. I find it to be a good practice to put a line label immediately above your error handling block. I typically use 9999 to indicate that the main part of the procedure ran to its expected conclusion.

Notes:

  • Line labels must be positive integers -- a label like MadeItHere: isn't recogonized by Erl.

  • Line labels are completely unrelated to the actual line numbers of a VBIDE CodeModule. You can use any positive numbers you want, in any order you want. In the example above, there are only 25 or so lines of code, but the line label numbers begin at 1000. There is no relationship between editor line numbers and line label numbers used with Erl.

  • Line label numbers need not be in any particular order, although if they are not in ascending, top-down order, the efficacy and benefit of Erl is greatly diminished, but Erl will still report the correct number.

  • Line labels are specific to the procedure in which they appear. If procedure ProcA calls procedure ProcB and an error occurs in ProcB that passes control back to ProcA, Erl (in ProcA) will return the most recently encounterd line label number in ProcA before it calls ProcB. From within ProcA, you cannot get the line label numbers that might appear in ProcB.

Use care when putting line number labels within a loop. For example,

For X = 1 To 100
500:
' some code that causes an error
600:
Next X

If the code following line label 500 but before 600 causes an error, and that error arises on the 20th iteration of the loop, Erl will return 500, even though 600 has been encountered successfully in the previous 19 iterations of the loop.

Proper placement of line labels within the procedure is critical to using the Erl function to get truly meaningful information.

There are any number of free utilities on the Internet that will insert numeric line label in a procedure automatically, so you have fine-grained error information while developing and debugging, and then remove those labels once code goes live.

If your code displays error information to the end user if an unexpected error occurs, providing the value from Erl in that information can make finding and fixing the problem vastly simpler than if value of Erl is not reported.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Chip Pearson
  • 51
  • 1
  • 2
4

The code below shows an alternative that ensures there is only one exit point for the sub/function.

function something() as ResultType
    Dim conn As ADODB.Connection
    Dim rst As ADODB.Recordset
    Dim res as ResultType
    ' Partial declaration block shown

    on error goto errHandler
    res.errMsg = ""

    do ' Dummy loop
        Set conn = initDB(databaseFilename)
        conn.BeginTrans
        Set rstCust = New ADODB.Recordset
        sql = "SELECT cust_name FROM customers"
        rstCust.Open sql, conn, adOpenKeyset, adLockOptimistic
        ....
        ....
        if needToExit then
            res.errMsg = "Couldn't stand the weather"
            exit do
        end if
        ....
        ....
        if gotToGetOutOfHere then
            exit do
        end if
        ....
    loop until true

    ' End of code. Single exit point for all above code

' Think of this as the 'exit' handler that
' handles both error and normal exits
errHandler:
    if Err.number <> 0 then
        res.errMsg = Err.description
    end if

    If Not rst Is Nothing Then
        If rst.State = adStateOpen Then
            rst.Close
        End If

        Set rst = Nothing
    End If

    If Not conn Is Nothing Then
        If res.errMsg = "" Then
            conn.CommitTrans
        Else
            conn.RollbackTrans
        End If

        conn.Close
        Set conn = Nothing
    End If

    something = res
end function
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
nickD
  • 141
  • 1
  • 4
  • i like this flow-thru style, but not sure what's the advantage of `Exit Sub` instead of `err.clear`. – johny why Sep 25 '21 at 04:21
  • 1
    @johnywhy I've re-arranged the code to make it clearer. I removed the err.clear and added the do...loop. I don't know why I left it out of the original post. – nickD Sep 26 '21 at 07:35
3

I use a piece of code that I developed myself and it is pretty good for my code:

In the beginning of the function or sub, I define:

On error Goto ErrorCatcher:

And then, I handle the possible errors

ErrorCatcher:
Select Case Err.Number

Case 0 ' Exit the code when no error was raised
    On Error GoTo 0
    Exit Function
Case 1 ' Error on definition of object
    'do stuff
Case... ' A little description here
    ' Do stuff
Case Else
    Debug.Print "###ERROR"
    Debug.Print "   • Number  :", Err.Number
    Debug.Print "   • Descrip :", Err.Description
    Debug.Print "   • Source  :", Err.Source
    Debug.Print "   • HelpCont:", Err.HelpContext
    Debug.Print "   • LastDLL :", Err.LastDllError
    Stop
    Err.Clear
    Resume
End Select
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
3

Here's a pretty decent pattern.

For debugging: When an error is raised, hit Ctrl + Break (or Ctrl + Pause), drag the break marker (or whatever it's called) down to the Resume line, hit F8 and you'll step to the line that "threw" the error.

The ExitHandler is your "Finally".

The hourglass will be killed every time. The status bar text will be cleared every time.

Public Sub ErrorHandlerExample()
    Dim dbs As DAO.Database
    Dim rst As DAO.Recordset

    On Error GoTo ErrHandler
    Dim varRetVal As Variant

    Set dbs = CurrentDb
    Set rst = dbs.OpenRecordset("SomeTable", dbOpenDynaset, dbSeeChanges + dbFailOnError)

    Call DoCmd.Hourglass(True)

    ' Do something with the RecordSet and close it.

    Call DoCmd.Hourglass(False)

    ExitHandler:
        Set rst = Nothing
        Set dbs = Nothing
        Exit Sub

    ErrHandler:
        Call DoCmd.Hourglass(False)
        Call DoCmd.SetWarnings(True)
        varRetVal = SysCmd(acSysCmdClearStatus)

        Dim errX As DAO.Error
        If Errors.Count > 1 Then
           For Each errX In DAO.Errors
              MsgBox "ODBC Error " & errX.Number & vbCrLf & errX.Description
           Next errX
        Else
            MsgBox "VBA Error " & Err.Number & ": " & vbCrLf & Err.Description & vbCrLf & "In: Form_MainForm", vbCritical
        End If

        Resume ExitHandler
        Resume

End Sub

Select Case Err.Number
    Case 3326 'This Recordset is not updateable
        'Do something about it. Or not...
    Case Else
        MsgBox "VBA Error " & Err.Number & ": " & vbCrLf & Err.Description & vbCrLf & "In: Form_MainForm", vbCritical
End Select

It also traps for both DAO and VBA errors. You can put a Select Case in the VBA error section if you want to trap for specific Err numbers.

Select Case Err.Number
    Case 3326 'This Recordset is not updateable
        'Do something about it. Or not...
    Case Else
        MsgBox "VBA Error " & Err.Number & ": " & vbCrLf & Err.Description & vbCrLf & "In: Form_MainForm", vbCritical
End Select
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
whistle britches
  • 137
  • 2
  • 10
3

I find the following to work best, called the central error handling approach.

Benefits

You have two modes of running your application: Debug and Production. In the Debug mode, the code will stop at any unexpected error and allow you to debug easily by jumping to the line where it occurred by pressing F8 twice. In the Production mode, a meaningful error message will get displayed to the user.

You can throw intentional errors like this, which will stop execution of the code with a message to the user:

Err.Raise vbObjectError, gsNO_DEBUG, "Some meaningful error message to the user"

Err.Raise vbObjectError, gsUSER_MESSAGE, "Some meaningful non-error message to the user"

'Or to exit in the middle of a call stack without a message:
Err.Raise vbObjectError, gsSILENT

Implementation

You need to "wrap" all subroutines and functions with any significant amount of code with the following headers and footers, making sure to specify ehCallTypeEntryPoint in all your entry points. Note the msModule constant as well, which needs to be put in all modules.

Option Explicit
Const msModule As String = "<Your Module Name>"

' This is an entry point
Public Sub AnEntryPoint()
    Const sSOURCE As String = "AnEntryPoint"
    On Error GoTo ErrorHandler

    'Your code

ErrorExit:
    Exit Sub

ErrorHandler:
    If CentralErrorHandler(Err, ThisWorkbook, msModule, sSOURCE, ehCallTypeEntryPoint) Then
        Stop
        Resume
    Else
        Resume ErrorExit
    End If
End Sub

' This is any other subroutine or function that isn't an entry point
Sub AnyOtherSub()
    Const sSOURCE As String = "AnyOtherSub"
    On Error GoTo ErrorHandler

    'Your code

ErrorExit:
    Exit Sub

ErrorHandler:
    If CentralErrorHandler(Err, ThisWorkbook, msModule, sSOURCE) Then
        Stop
        Resume
    Else
        Resume ErrorExit
    End If
End Sub

The contents of the central error handler module is the following:

'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Comments: Error handler code.
'
'           Run SetDebugMode True to use debug mode (Dev mode)
'           It will be False by default (Production mode)
'
' Author:   Igor Popov
' Date:     13 Feb 2014
' Licence:  MIT
'
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Option Explicit
Option Private Module

Private Const msModule As String = "MErrorHandler"

Public Const gsAPP_NAME As String = "<You Application Name>"

Public Const gsSILENT As String = "UserCancel"  'A silent error is when the user aborts an action, no message should be displayed
Public Const gsNO_DEBUG As String = "NoDebug"   'This type of error will display a specific message to the user in situation of an expected (provided-for) error.
Public Const gsUSER_MESSAGE As String = "UserMessage" 'Use this type of error to display an information message to the user

Private Const msDEBUG_MODE_COMPANY = "<Your Company>"
Private Const msDEBUG_MODE_SECTION = "<Your Team>"
Private Const msDEBUG_MODE_VALUE = "DEBUG_MODE"

Public Enum ECallType
    ehCallTypeRegular = 0
    ehCallTypeEntryPoint
End Enum

Public Function DebugMode() As Boolean
    DebugMode = CBool(GetSetting(msDEBUG_MODE_COMPANY, msDEBUG_MODE_SECTION, msDEBUG_MODE_VALUE, 0))
End Function

Public Sub SetDebugMode(Optional bMode As Boolean = True)
    SaveSetting msDEBUG_MODE_COMPANY, msDEBUG_MODE_SECTION, msDEBUG_MODE_VALUE, IIf(bMode, 1, 0)
End Sub

'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Comments: The central error handler for all functions
'           Displays errors to the user at the entry point level, or, if we're below the entry point, rethrows it upwards until the entry point is reached
'
'           Returns True to stop and debug unexpected errors in debug mode.
'
'           The function can be enhanced to log errors.
'
' Date          Developer           TDID    Comment
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' 13 Feb 2014   Igor Popov                  Created

Public Function CentralErrorHandler(ErrObj As ErrObject, Wbk As Workbook, ByVal sModule As String, ByVal sSOURCE As String, _
                                    Optional enCallType As ECallType = ehCallTypeRegular, Optional ByVal bRethrowError As Boolean = True) As Boolean

    Static ssModule As String, ssSource As String
    If Len(ssModule) = 0 And Len(ssSource) = 0 Then
        'Remember the module and the source of the first call to CentralErrorHandler
        ssModule = sModule
        ssSource = sSOURCE
    End If
    CentralErrorHandler = DebugMode And ErrObj.Source <> gsNO_DEBUG And ErrObj.Source <> gsUSER_MESSAGE And ErrObj.Source <> gsSILENT
    If CentralErrorHandler Then
        'If it's an unexpected error and we're going to stop in the debug mode, just write the error message to the immediate window for debugging
        Debug.Print "#Err: " & Err.Description
    ElseIf enCallType = ehCallTypeEntryPoint Then
        'If we have reached the entry point and it's not a silent error, display the message to the user in an error box
        If ErrObj.Source <> gsSILENT Then
            Dim sMsg As String: sMsg = ErrObj.Description
            If ErrObj.Source <> gsNO_DEBUG And ErrObj.Source <> gsUSER_MESSAGE Then sMsg = "Unexpected VBA error in workbook '" & Wbk.Name & "', module '" & ssModule & "', call '" & ssSource & "':" & vbCrLf & vbCrLf & sMsg
            MsgBox sMsg, vbOKOnly + IIf(ErrObj.Source = gsUSER_MESSAGE, vbInformation, vbCritical), gsAPP_NAME
        End If
    ElseIf bRethrowError Then
        'Rethrow the error to the next level up if bRethrowError is True (by Default).
        'Otherwise, do nothing as the calling function must be having special logic for handling errors.
        Err.Raise ErrObj.Number, ErrObj.Source, ErrObj.Description
    End If
End Function

To set yourself in the Debug mode, run the following in the Immediate window:

SetDebugMode True
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
igorsp7
  • 441
  • 2
  • 4
2

My personal view on a statement made in previous answers:

And just for fun:

On Error Resume Next is the devil incarnate and to be avoided, as it silently hides errors.

I'm using the On Error Resume Next on procedures where I don't want an error to stop my work and where any statement does not depend on the result of the previous statements.

When I'm doing this I add a global variable debugModeOn and I set it to True. Then I use it this way:

If not debugModeOn Then On Error Resume Next

When I deliver my work, I set the variable to false, thus hiding the errors only to the user and showing them during testing.

Also using it when doing something that may fail like calling the DataBodyRange of a ListObject that may be empty:

On Error Resume Next
Sheet1.ListObjects(1).DataBodyRange.Delete
On Error Goto 0

Instead of:

If Sheet1.ListObjects(1).ListRows.Count > 0 Then
    Sheet1.ListObjects(1).DataBodyRange.Delete
End If

Or checking existence of an item in a collection:

On Error Resume Next
Err.Clear
Set auxiliarVar = collection(key)

' Check existence (if you try to retrieve a nonexistent key you get error number 5)
exists = (Err.Number <> 5)
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Jordi
  • 2,055
  • 1
  • 16
  • 34
  • 1
    >`If not debugModeOn Then On Error Resume Next` In this case it is better to use conditional compilation like `#If Hide_Errors > 0 Then On Error Resume Next` and set `Hide_Errors` in the VBA project properties `Conditional Complication Arguments` accordingly. – Aleksey F. Jun 06 '15 at 05:32
  • @AlekseyF. Some apps may have numerous custom config options, not just Hide_Errors. In that case, i'll create a table on a worksheet to hold (and lookup) various config options (including execution-mode). – johny why Sep 25 '21 at 04:16
  • @johnywhy, the purpose is to get the target code without unused commands to eliminate their execution at all rather than to place a config into the VBA project properties. – Aleksey F. Sep 26 '21 at 08:10
  • @AlekseyF. Not sure you understand me. I use the same method as you, except instead of storing Hide_Errors in the VBA project properties, i'll store Hide_Errors on a worksheet. Behavior is same. – johny why Sep 26 '21 at 16:45
  • 1
    @johnywhy, yes, behavior is the same. However, owing to conditional compilation, the internal code being executed is not: the lines of code are excluded from the compiled code conditionally, i.e. there are no conditional statements and others, and VBA engine does not process them at all in runtime, i.e. even conditions in `if`-statements are not calculated and no CPU time is used. – Aleksey F. Sep 26 '21 at 17:23
2

Beware of the elephant trap:

I saw no mention of this in this discussion. Access 2010

How ACCESS/VBA handles errors in CLASS objects is determined by a configurable option:

VBA Code Editor → ToolsOptionsGeneralError Trapping:

Enter image description here

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
JoeRobbins
  • 21
  • 2
1

Error-handling has always been a bother. I've experimented with various techniques. Here's my solution.

This approach brings together my preferred methods on this SO page, plus a few of my own techniques.

The question asker mentions only the simple case -- a single procedure. I also cover sub-procedures, custom errors, logging, error-related processing, and other error-related topics.

No error-handling

The simplest case: Don't assume you always need handling. Procedures which are never going to error out don't need error-handling.

Ignored Errors

It's acceptable to simply ignore some errors. This is perfectly acceptable example of an ignored error, because you know there's no other error that can reasonably occur on that statement.

...
On Error Resume Next
Set bkCars = Workbooks("Cars.xlsx")
On Error GoTo 0
If (bkCars Is Nothing) Then MsgBox "Cars workbook isn't open."
Set bkCars = Workbooks("Wheelbarrows.xlsx")
...

I've never heard of any other error ever happening on that statement. Use your judgement. Ignore extremists. VBA is supposed to be easy. On Error Resume Next isn't "the Devil incarnate"; it's one way to implement Try..Catch in VBA. For more examples, see Jordi's answer.

Unhandled Errors

The remainder of this answer is about unhandled errors. An unhandled error is an unexpected error which breaks your program.

Handler

Here's my basic handler:

Sub Main()
          On Error GoTo HANDLER

          Dim x As Long
          x = "hi"

HANDLER:
          ' cleanup
          x = 0

          ' error-handler
          If (Err = 0) Then Exit Sub
          MsgBox Error
End Sub
  • Flow-through: Inspired by @NickD and others here, it completely eliminates "Exit Sub" and "Resume" from your code. Code flows in one direction, instead of jumping around. There's a single exit point in the procedure. All are important, if you like less typing, less code, and less spaghetti.
  • *Cleanup: This approach ensures the same cleanup code runs whether there is or isn't an error. Error and non-error conditions share the same cleanup code. Straightforward pattern handles a wide variety of scenarios regarding cleanup and custom-handling.

Convenience

Make your life easier.

Function IsEr() As Boolean
      IsEr = (Err <> 0)
      ' or IsEr = CBool(Err)
End Function

Special Handlers

The basic style can handle more complex cases. For example, you can insert handling or cleanup for specific errors.

          ...
HANDLER:
          If Not IsEr Then Exit Sub
          If (Err = 11) Then Call_TheBatphone
          MsgBox Error
End Sub

Procedure Calls, No Cleanup

A called procedure which doesn't have any special cleanup code doesn't need any error-code. It's errors, and those of it's sub-procedures, will automatically bubble up to the entry-procedure. You can have cleanup-code at each sub.

Sub Main()
          On Error GoTo HANDLER
          Sub_1
HANDLER:
          If Not IsEr Then Exit Sub
          MsgBox Error
End Sub


Sub Sub_1()
          Dim x
          x = 5/0    <.. will jump to Main HANDLER
End Sub

Procedure Calls, Cleanup

However, a sub-procedure which must always run cleanup-code (even in case of an error) needs a bit of extra help. The sub's error-handler resets the error-event, so the error must be retriggered with Err.Raise.

This means your handler for subs must be different than the handler for the kickoff-procedure (aka "entry-point", meaning the first procedure that runs at the beginning of the roundtrip code-loop).

Sub-handlers shouldn't show any message boxes or do any logging -- that should remain with the Main handler. Sub handlers should only be used for special cleanup, special processing, and to append extra error-info to the error object.

Sub Main()
          On Error GoTo HANDLER
          Sub_1
HANDLER:
          If Not IsEr Then Exit Sub
          MsgBox Error
End Sub


Sub Sub_1()
          On Error GoTo HANDLER
          Dim x
          x = 5/0
          ' More processing here
HANDLER:
          If Not IsEr Then Exit Sub
          Err.Raise Err.Number, Err.Source, Err.Description & vbNewline & "Some problem with divisor"
End Sub

Run

Beware: any procedure executed with the Run statement requires special handling. If you raise an error within the procedure, the error will not bubble up to the entry procedure, and whatever into the Raise puts into the Err will be lost when execution returns to the caller. Therefore, you need to create a workaround. My workaround is to put Err.Number into a global variable, and then on return from the Run, check that variable.

Public lErr As Long


Sub Main()
          On Error GoTo HANDLER
          Run "Sub_1"

          If (lErr <> 0) then Err.Raise lErr

          Dim x
          x = 5
HANDLER:
          If Not IsEr Then Exit Sub
          MsgBox Error
End Sub


Sub Sub_1()
          On Error Goto HANDLER

          Dim x
          ' will NOT jump to Main HANDLER, since Run
          x = 5/0
HANDLER:
          If (Err <> 0) Then lErr = Err
End Sub

Alerts

If your intention is produce professional code, then you must communicate all unexpected errors appropriately to the user, as shown above. You never want users to see a "Debug" button or find themselves dropped into VBA.

Centralized Handling

The next evolution is centralized handling. This gives you a really quick and easy way to replicate your perfect error-handling everywhere. As mentioned by @igorsp7, centralized handling makes it simpler and easier to implement consistent, reliable error-handling everywhere. It makes it easy to reuse complex handler logic. It is so easy and simple to just place ErrorHandler at the bottom of every procedure. Reminder: Err is a global object, so there's no need to pass it around as an argument.

Sub Main()
          On Error GoTo HANDLER
          Sub_1
HANDLER:
          MainCleanup
          ErrorHandler_Entry
End Sub


Sub Sub_1()
          On Error GoTo HANDLER
          Dim x
          x = 5/0
HANDLER:
          SubCleanup
          ErrorHandler_Sub
End Sub


Sub ErrorHandler_Entry()
          If Not IsEr Then Exit Sub

          ' log error to a file for developer to inspect.
          Log_Error_To_File

          ' Then alert user. InputBox provides simple way to let users copy with mouse
          InputBox "Sorry, something went haywire. Please inform the developer or owner of this application.", _
                    "Robot Not Working", Err.Number & vbNewLine & Err.Source & vbNewLine & Err.Description
End Sub


Private Sub ErrorHandler_Sub()
          If Not IsEr Then Exit Sub

          ' bubble up the error to the next caller
          Err.Raise Err.Number, Err.Source, Err.Description
End Sub

Custom Errors

Numbering

Use = vbObjectError + 514 for your first one, as 1 to 513 are reserved for native VB errors. I'm still researching custom error numbering. There's a lot of conflicting information. It may be simply

  • Native errors are positive integers, to 65535?
  • Custom errors are negative integers, 0 to -2,000,000,000?

But I don't know yet if that's correct! Your error handlers will work even if you use native error numbers. However, if your error handling is based on whether it's a native vs custom error, or if your application is reporting the error to a developer, then to avoid confusion or more bugs, the best practice is to not reuse native numbers.

Syntax

Enum CustomError
          UserPause = vbObjectError + 514
          UserTerminate
End Enum


Function CustomErr()as Boolean
          CustomErr = (Err >= 514)
End Function


Sub Test
          On Error Goto HANDLER
          Err.Raise CustomError.UserPause
HANDLER:
          Cleanup
          If CustomErr Then Handle_CustomError
End Sub


Sub Handle_CustomError()
          Select Case Err
                    Case UserPause
                              MsgBox "Paused"
                              Resume Next
                    Case UserTerminate
                              SpecialProcessing
                              MsgBox "Terminated"
                              End
          End Select
End Sub

Error Categories:

You may want custom errors in an addin, an application workbook, and a data workbook. You should reserve a range of allowed error numbers for each type. Then your handlers can determine the source of the error by its number. This enum uses the starting number for each range.

Enum AppError
          UserPause = vbObjectError + 514
          UserTerminate
End Enum


Enum AddinError
          LoadFail = vbObjectError + 1000
End Enum


Enum DataError
          DatabaseLocked = vbObjectError + 1500
End Enum


Enum ErrorType
    VB
    App
    Addin
    Data
End Enum


Function Get_ErrorCategory() As ErrorType
          If (Err < 514) Then
                    Get_ErrorCategory = VB

          ElseIf (Err <= 1000) Then
                    Get_ErrorCategory = App

          ElseIf (Err <= 1500) Then
                    Get_ErrorCategory = Addin

          Else
                    Get_ErrorCategory = Data
          End If
End Function


Sub ErrorHandler_Entry(Optional sInfo As String)
          If Not IsEr Then Exit Sub

          Select Case Get_ErrorCategory
                    Case VB
                              InputBox "Sorry, something went haywire. Please inform the developer or owner of this application.", _
                                        "Robot Not Working", Err.Number & vbNewLine & Err.Source & vbNewLine & Err.Description & vbNewLine & sInfo

                    Case Addin
                              Log_Error_To_File

                    Case Data
                              ' do nothing
          End Select
End Sub

Developer Mode

As developer, you'll want to debug unhandled errors, instead of getting friendly messages. So you want to temporarily disable your handler when you're in development. That's conveniently done by manually setting a "Debug" state someplace. There are a couple of ways to do it:

Custom "ExecMode":

Get_DebugMode is a function that you need to write, which pulls your Debug mode from wherever you stored it. Can be stored in an Excel defined-name, a module constant, a worksheet cell -- whatever you prefer.

      ...
      If Not Get_DebugMode Then _
                On Error GoTo HANDLER
      ...

Conditional Compilation Arguments:

This needs to be applied in the VB IDE.

      ...
      #If Not DEBUGMODE Then _
                On Error GoTo HANDLER
      ...

Enter image description here

Changing code behavior at compile time

johny why
  • 2,047
  • 7
  • 27
  • 52