Error handling in a Class Module Function - vba

I have several Class Modules in my Workbook. One of public functions in Class Module 1 relies on data from a function in Class Module 2, this happens four times. In the event that the object is missing from Class 2 the program crashes (as expected). I am having difficulty debugging the code properly as it seems that I can only exit the program from the main subroutine. I would prefer to kill the program from the Class function, but I don't know if that is possible (we can squash this here if it is). I am currently using On Error statements in the main subroutine, but these don't execute in a timely manner because the function in Class 1 gets data from Class 2 four times.
Class Module 1 Function
Function oload(ByVal pload As Double, ByVal cord As cCordCol, ByVal grid As cGridCol)
' cord is a scripting.dictionary of Class Module Objects (cCord)
' grid is a scripting.dictionary of Class Module Objects (cGrid)
n1 = grid.Item(pg1).toGlobal(cord)
n2 = grid.Item(pg2).toGlobal(cord)
n3 = grid.Item(pg3).toGlobal(cord)
n4 = grid.Item(pg4).toGlobal(cord)
' do something here
oload = sum_Ploads
End Function
Above the n1 thru n4 is where I am calling on the public function of Class Module 2.
Below is the Class Module 2 Function
Function toGlobal(ByVal cord As cCordCol)
On Error Resume Next
ctype = cord.Item(Me.cord1).sys
' Missing Coordinate System Error
If Err.Number <> 0 Then
i = MsgBox("The definition of Coordinate " & Me.cord1 & " was missing from the Bulk Data " & Chr(10) & _
"File. Include this Coord in the .bdf and re-execute the program.", vbOKOnly, "Runtime Error")
' *** TERMINATE MAIN SUBROUTING HERE ***
End If
This will raise a message box indicating that an object was missing, specifically the (me.cord1) part - this is an item in a scripting.dictionary. I want to terminate the program here.
The main subroutine (greatly reduced) is here:
Sub main()
' lookup Element ID, Calc OLOAD, Sum Load Set OLOAD
On Error GoTo PROC_ERR
If dict_quad.Exists(EID) Then dict_oload.Item(LS).add_to_oload (dict_quad.Item(EID).oload(pload, dict_cord, dict_grid))
If dict_tria.Exists(EID) Then dict_oload.Item(LS).add_to_oload (dict_tria.Item(EID).oload(pload, dict_cord, dict_grid))
PROC_ERR:
If Err.Number <> 0 Then Exit Sub
End Sub
I have a lot of nested operations here. You can see the "goto" statement won't execute until the "oload" function is complete. The "oload" function calls upon the "toGlobal" function four times before it finishes its calculations.
How can I terminate the subroutine after the first occurance of a missing object in the "toGlobal" function?

Your On Error Resume Next statement is swallowing the error in toGlobal and preventing it from "bubbling up" the call stack. One option you have is to "re-throw" the error. Doing this in VBA is a bit kludgier than other languages, but is still possible.
For example, you can change the toGlobal function to something like the following:
Function toGlobal(ByVal cord As cCordCol)
On Error Resume Next
ctype = cord.Item(Me.cord1).sys
' Missing Coordinate System Error
If Err.Number <> 0 Then
i = MsgBox("The definition of Coordinate " & Me.cord1 & " was missing from the Bulk Data " & Chr(10) & _
"File. Include this Coord in the .bdf and re-execute the program.", vbOKOnly, "Runtime Error")
' *** TERMINATE MAIN SUBROUTING HERE ***
'Save info from the error object (it will get cleared in the On Error statement below)
Dim ErrNum As Long: ErrNum = Err.Number
Dim ErrMsg As String: ErrMsg = Err.Description
Dim ErrSrc As String: ErrSrc = Err.Source
'Reset error handling to allow errors to bubble up the call stack
On Error Goto 0
'"Re-throw" the error
Err.Raise ErrNum, "toGlobal:" & ErrSrc, ErrMsg
End If
The other option is to just remove your On Error Resume Next statement from your class module so that the error simply bubbles up naturally. This is the approach that I would generally take unless there was some compelling reason to stray from it.

Related

How to resume error handling from the calling function while in a subfunction in VBA?

When I write in VBA for Word or Excel, I typically have an error handler in my main function and call several subs from it, and most of the time, I want subs' messages to get caught in the main function. Typically everything works great with this strategy, and it mimics what I'm used to in C++.
However, I run into trouble when I need a different type of error handling in one or two subs.
For example, when I need to turn on Resume Next for the sake of checking if an object fails and is set to nothing. When I want to turn error handling on, my MainErrorHandler is now out of scope.
Sub Main()
On Error GoTo MainErrorHandler
Application.ScreenUpdating = False
Call OpenFile
Call SubWithOwnErrorHandling
'Do more stuff
GoTo CleanExit
MainErrorHandler:
MsgBox Err.Description
CleanExit:
Application.ScreenUpdating = True
End Sub
Sub OpenFile()
On Error Resume Next
Set objFile = objFSO.OpenTextFile(fileLocation & fileName, 1)
On Error GoTo ErrorHandler ' Label Not Defined!
If objFile Is Nothing Then
Call Err.Raise(2009, , "Out File doesn’t exist.")
End If
End Sub
Likewise, when I want to have a sub handle errors locally and occasionally elevate an error, I'm not sure how exactly to do that.
Sub SubWithOwnErrorHandling()
On Error GoTo SubErrorHandler
isReallyBad = True
If isReallyBad Then
Call Err.Raise(2020, , "Error that needs to cause application to exit!")
Else
Call Err.Raise(2001, , "Error that just needs the function to exit!")
End If
SubErrorHandler:
On Error GoTo MainErrorHandler ' Label Not Defined!
If Err.Number = 2020 Then
Call Err.Raise(2020, , Err.Description)
End If
End Sub
Is there any way to do what I'm trying to accomplish for either case?
Labels are always local.
On Error is always local too - heck, its deprecated ancestor was On Local Error!
So you can't GoTo-jump between procedure scopes (THANK GOD!!)
This means at any given time, there's only ever one of two things the run-time can do On Error:
Jump to a local error handler
Blow up the current stack frame and see if the caller handles it
[ignore the error and happily keep running blindfolded under blue skies and sunshine]
That third point, you guessed it, is what On Error Resume Next does.
One critical error you've done, is specifying an On Error statement inside an error-handling subroutine, and the error-handling subroutine runs regardless of whether you're in an error state or not. That makes following execution extremely confusing, even if that label was legal. Exit Sub or Exit Function (or heck, Exit Property, depending on what's your scope) before the handler, and make sure error-handling code is only ever hit in an error state.
Resetting error handling
So, one thing you want to do, is to reset error handling - here:
On Error Resume Next
Set objFile = objFSO.OpenTextFile(fileLocation & fileName, 1)
On Error GoTo ErrorHandler ' Label Not Defined!
You know objFSO.OpenTextFile can possibly blow up, and you want to handle it yourself, i.e. deal with the objFile Is Nothing possibiilty manually. You can absolutely do that, but then what you need is this:
On Error Resume Next
Set objFile = objFSO.OpenTextFile(fileLocation & fileName, 1)
On Error GoTo 0
On Error GoTo 0 resets error handling, i.e. the next instruction to throw an error will bubble up the call stack, until everything goes up in flames.
Custom Errors
The next thing you want to do, is to raise custom errors.
If isReallyBad Then
Call Err.Raise(2020, , "Error that needs to cause application to exit!")
Else
Call Err.Raise(2001, , "Error that just needs the function to exit!")
End If
That's pretty easy actually - but it's easier with an Enum:
Public Enum AppCustomError
ERR_ReallyBad = vbObjectError + 42
ERR_ReallyReallyBad
ERR_VeryReallyTerriblyBad
ERR_YouGetTheIdea
End Enum
The vbObjectError constant ensures that your custom error numbering doesn't step on toes; your error numbers will all be negative - and with an Enum for each possible error you can throw, you don't need to care what the actual error number is, so you let the enum member mechanics do their thing (e.g. ERR_ReallyReallyBad will be ERR_ReallyBad + 1, automatically).
Then you can do this (assuming you're in a class module - otherwise replace TypeName(Me) with some string literal, or skip it):
On Error GoTo ErrHandler
If isReallyBad Then
Err.Raise ERR_VeryReallyTerriblyBad, TypeName(Me), "Blow up the app!"
Else
Err.Raise ERR_ReallyBad, TypeName(Me), "Blow up this function!"
End If
Exit Sub
ErrHandler:
With Err
Select Case .Number
Case ERR_VeryReallyTerriblyBad
.Raise .Number 'rethrow
Case ERR_ReallyBad
'function blew up, we're done here.
'...
End Select
End With
And then the calling code, which has its own error-handling subroutine, can decide that it can't deal with ERR_VeryReallyTerriblyBad, and just blow everything up by rethrowing:
Exit Sub
MainErrorHandler:
With Err
Select Case .Number
Case ERR_VeryReallyTerriblyBad
.Raise .Number 'rethrow
Case Else
MsgBox .Description
End Select
End With

Excel VBA macros and error messages

I'm very inexperienced in Excel and I'm running into a problem programming a macro. It simply stops without any error dialog, no matter whether I set a breakpoint and step over or not.
Am I missing something? Do I have to turn on error messages explicitly?
Something like this, in a module:
Function Provoke(XYZ As Integer) As Integer
Provoke = XYZ / 0
End Function
The Function is used in a formula inside of a cell, like this
=Provoke(4711)
How can I make it complain about the division by zero?
If an error occurs in a Function when called from a worksheet as a UDF, you won't see any error messages- you just get a #VALUE error returned to the cell(s)
If you call that same function from a Sub in VBA, you will see the error.
If you want to be able to return more-specific error messages you'll need to to a little more coding.
E.g.
Function Provoke(XYZ As Integer)
On Error GoTo haveError
Provoke = XYZ / 0
Exit Function
haveError:
'what type of error did we get?
Select Case Err.Number
Case 11: Provoke = CVErr(xlErrDiv0)
Case Else: Provoke = "Error!"
End Select
End Function
A good reference for this: http://www.cpearson.com/excel/ReturningErrors.aspx
Yes you need to make provision to tell VBA what to do if error occurs.
The error is occuring because of this line in your code:
Provoke = XYZ / 0
No number is divisible by zero and it generates an error. Basic math: any number ÷ 0 = ∞.
To verify firsthand, you may try something like this to trap the error number as well as source and description:
Function Provoke(XYZ As Integer)
On Error Resume Next
Provoke = XYZ / 0
If Err.Number <> 0 Then Provoke = "Error# " & Err.Number & " - generated by " & Err.Source & " - " & Err.Description
On Error GoTo 0
End Function
Now, if you try
=Provoke(4711)
You'll get the result like:
Error# 11 - generated by VBAProject - Division by zero

vba nested error handling

I am trying to understand how to set up error-handling for the situation cited below. Sub A has error handling for its own context. However it calls Sub B which creates database connection, resultsets etc which it cleans up in its error handler. The way it stands, it seems only ErrorHandler A is active throughout. How can I activate ErrorHandler B when B is called and revert back to error-handling after B returns.
Thanks
Sub A
' Preps for database access
On Error GoTo ErrorHandlerA
B
.
.
Exit Sub
ErrorHandlerA:
...
Resume
End Sub
Sub B
' Does database access stuff
On Error ErrorHandlerB
cleanUp:
con.close
rs.close
Exit Sub
ErrorHandlerB:
GoTo cleanup
End Sub
Exit Sub
ErrorHandlerA:
...
Resume
End Sub
Resume here, resumes normal execution (clears "error handling mode") and jumps back to the instruction that caused the error. Presumably (hopefully?) the ... contains code that makes the previously-failing call, succeed.
cleanUp:
con.close
rs.close
Exit Sub
ErrorHandlerB:
GoTo cleanup
End Sub
Here you're GoTo-jumping outside the error-handling subroutine, without resuming to normal execution, so the instructions under the cleanUp line label execute in an error-handling context. That should be Resume cleanUp, not GoTo.
The Exit Sub statement leaves the scope, which clears the error state and implicitly resumes to normal execution. Here's a little MCVE:
Public Sub Test()
On Error GoTo A
DoSomething
Debug.Print "After DoSomething: " & Err.Number
Exit Sub
A:
Debug.Print "Inside Test[A]: " & Err.Number
End Sub
Private Sub DoSomething()
On Error GoTo A
Err.Raise 5
B:
Debug.Print "Inside DoSomething[B]: " & Err.Number
Exit Sub
A:
Debug.Print "Inside DoSomething[A]: " & Err.Number
Resume B
End Sub
Running the Test procedure produces this output:
Inside DoSomething[A]: 5
Inside DoSomething[B]: 0
After DoSomething: 0
Changing the Resume B instruction for a GoTo B produces this output:
Inside DoSomething[A]: 5
Inside DoSomething[B]: 5
After DoSomething: 0
As you can see, as the error was handled in DoSomething, error state is reset when execution returns to Test, and the handler in the Test procedure is never triggered.
If you want to propagate the error to the caller, you have a number of options:
Don't handle errors in the called procedure. In the above example this means DoSomething wouldn't have an On Error statement; any run-time errors would "bubble up" the call stack, and the output would look like this now:
Inside Test[A]: 5
You would typically do this when it's the calling code that has the knowledge of what the best course of action would be (e.g. display a MsgBox to the user, or log the error, etc.).
Handle errors, and then re-raise them. In your example this means moving (or copying) the cleanup code into the ErrorHandlerB subroutine, and calling Err.Raise Err.Number instead of Resume. Or you keep the GoTo and then the cleanup subroutine can do If Err.Number <> 0 Then Err.Raise Err.Number to effectively re-throw the error for the caller to also handle. In other words:
Public Sub Test()
On Error GoTo A
DoSomething
Debug.Print "After DoSomething: " & Err.Number
Exit Sub
A:
Debug.Print "Inside Test[A]: " & Err.Number
End Sub
Private Sub DoSomething()
On Error GoTo A
Err.Raise 5
B:
Debug.Print "Inside DoSomething[B]: " & Err.Number
If Err.Number <> 0 Then Err.Raise Err.Number
Exit Sub
A:
Debug.Print "Inside DoSomething[A]: " & Err.Number
GoTo B 'resuming would clear the error and prevent rethrow
End Sub
This outputs:
Inside DoSomething[A]: 5
Inside DoSomething[B]: 5
Inside Test[A]: 5
Depending on the exact reason why you would want to handle the same error in two different places, you could also consider making B a Function that returns a Boolean indicating success or failure - in that case A no longer handles an error per se, but rather uses normal flow control to determine what to do:
Public Sub Test()
On Error GoTo A
If Not DoSomething Then
Debug.Print "After DoSomething failed: " & Err.Number
Exit Sub
End If
Exit Sub
A:
Debug.Print "Inside Test[A]: " & Err.Number
End Sub
Private Function DoSomething() As Boolean
'Dim success As Boolean
On Error GoTo A
Err.Raise 5
'success = True
B:
Debug.Print "Inside DoSomething[B]: " & Err.Number
DoSomething = (Err.Number = 0) 'DoSomething = success
Exit Function
A:
Debug.Print "Inside DoSomething[A]: " & Err.Number
'success = False
GoTo B 'Resume B
End Function
Which produces this output:
Inside DoSomething[A]: 5
Inside DoSomething[B]: 5
After DoSomething failed: 0
Notice Test knows DoSomething failed, but Err.Number is 0. This is usually preferable to using runtime errors for flow control, again depending on your actual scenario. Also notice, the GoTo jump is avoidable in that situation, using a simple Boolean local variable to track your return value (the commented-out code).

Is there a way I can avoid using GoTo in this situation?

So I've got a sub (triggered by a command button) which runs a process that seems quite time-consuming (between 5 and 20 seconds, dependant on the machine and how co-operative our network is feeling). To make it clear to the user that stuff is happening that they can't see I change the mouse pointer to an hourglass then change it back when the sub exits, regardless of the reason for the exit.
With that in mind my code looks something like this (illustrative example, not actual code):
Private Sub cmdDoTheThing_Click()
On Error GoTo Err_cmdDoTheThing
Screen.MousePointer = 11 'Hourglass/Equivalent
'Check all data is available to Do The Thing
If Not MyModule.ThingIsDoable(Me.PrimaryKey) Then
MsgBox "Cannot Do The Thing, more preliminary things must be done first."
GoTo Exit_cmdDoTheThing
End If
'Try to Do The Thing (function returns false on failure)
If Not MyModule.DoTheThing(Me.PrimaryKey) Then
MsgBox "Processing of The Thing failed."
GoTo Exit_cmdDoTheThing
End If
'...
'Stuff here I don't want to do if either of the above failed
'...
Exit_cmdDoTheThing:
Screen.MousePointer = 0 'Default mouse pointer
Exit Sub
Err_cmdDoTheThing:
MsgBox "Error " & Err.Number & ": " & Err.Description
Resume Exit_DoTheThing
End Sub
I don't want to repeat Screen.MousePointer = 0 at every possible exit point so I figured a GoTo would serve as a decent shortcut since the Exit_cmdDoTheThing label was needed for error handling anyway.
Is this a valid use-case for a GoTo statement and if not is there some other way I can achieve the same result? I don't want a sudden raptor attack after all.
GoTo can be replaced by using a do-while block (which has a false condition and runs only once) and using 'Exit Do' wherever you want to skip the rest of the code.
So your code might now look like:
Private Sub cmdDoTheThing_Click()
On Error GoTo Err_cmdDoTheThing
Do
Screen.MousePointer = 11 'Hourglass/Equivalent
'Check all data is available to Do The Thing
If Not MyModule.ThingIsDoable(Me.PrimaryKey) Then
MsgBox "Cannot Do The Thing, more preliminary things must be done first."
Exit Do
End If
'Try to Do The Thing (function returns false on failure)
If Not MyModule.DoTheThing(Me.PrimaryKey) Then
MsgBox "Processing of The Thing failed."
Exit Do
End If
'...
'Stuff here I don't want to do if either of the above failed
'...
Loop While FALSE
Exit_cmdDoTheThing:
Screen.MousePointer = 0 'Default mouse pointer
Exit Sub
Err_cmdDoTheThing:
MsgBox "Error " & Err.Number & ": " & Err.Description
Resume Exit_DoTheThing
End Sub
GoTo has to be used with real caution as it may make the code really complex after some iterations in the code. GoTo also allows you to do very weird/ugly things such as jump out of scope without actually going out of scope. With the do-while you ensure the flow of the code while maintaining the sanity and readability of the code.
Raising a custom error can avoid the use of GoTo for subroutines with this error handling structure. This has the added benefit of making it clear to anyone reading the code that failure of certain functions to complete is considered an error in this situation, even if they do not raise an error upon failure.
Public Const cCustomErrNum = 9114
Private Sub cmdDoTheThing_Click()
On Error GoTo Err_cmdDoTheThing
Screen.MousePointer = 11 'Hourglass/Equivalent
'Check all data is available to Do The Thing
If Not MyModule.ThingIsDoable(Me.PrimaryKey) Then
Err.Raise cCustomErrNum,"cmd_DoTheThing_Click()", _
"Cannot Do The Thing, more preliminary things must be done first."
End If
'Try to Do The Thing (function returns false on failure)
If Not MyModule.DoTheThing(Me.PrimaryKey) Then
Err.Raise cCustomErrNum,"cmd_DoTheThing_Click()", _
"Processing of The Thing failed."
End If
'...
'Stuff here I don't want to do if either of the above failed
'...
Exit_cmdDoTheThing:
Screen.MousePointer = 0 'Default mouse pointer
Exit Sub
Err_cmdDoTheThing:
MsgBox "Error " & Err.Number & ": " & Err.Description
Resume Exit_DoTheThing
End Sub

What are some good patterns for VBA error handling? [closed]

Closed. This question is opinion-based. It is not currently accepting answers.
Want to improve this question? Update the question so it can be answered with facts and citations by editing this post.
Closed 9 months ago.
The community reviewed whether to reopen this question 6 months ago and left it closed:
Original close reason(s) were not resolved
Improve this question
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?
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
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
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.
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
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.
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.
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
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
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
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
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)
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 → Tools → Options → General → Error Trapping:
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
...
Changing code behavior at compile time