I have a project with a number of instances of
DoCmd.Hourglass -1
<various code>
DoCmd.Hourglass 0
<other code>
Any time there is an error, the line turning off the hourglass gets skipped. The users get confused and wait for the app to be ready again.
Normally, I'd handle this in a try..finally
try {
DoCmd.Hourglass -1
<various code>
} finally {
DoCmd.Hourglass 0
}
but that isn't an option in VBA.
I thought it would be a simple matter to search and find the correct approach, but every sample I found takes the naïve approach above.
The trick is that I don't want to affect any of the existing program flow, which means that I need to rethrow the error.
And, of course, "throw" doesn't exist either.
But it also means that I do NOT need to resume where the error happened.
Here's my current best guess
On Error GoTo ErrHandler
DoCmd.Hourglass -1
<various code>
DoCmd.Hourglass 0
<other code>
Exit Function
ErrHandler:
DoCmd.Hourglass 0
Err.Raise Err.Number
End Function
but I have 0 confidence that this is the best solution.
Looks like the question is answered here:
https://stackoverflow.com/a/64478691/1513027
err.raise Err.Number
This is the correct method, and no, you don't have to re-state all the err properties. The values are retained.
Related
I'm using GoTo for error handling in an Access module and am getting a type mismatch error on the Procedures.HandleError call.
I tested to see if err is an Error:
Exit Sub
catch:
If IsError(err) Then
MsgBox "yes"
Else
MsgBox "no"
End If
Procedures.HandleError "ctrCreateSubject, frm_OnCreate", err, True
End Sub
and the MsgBox displays no and I can't figure out why. I'm using the same syntax in other places without problems
Can anyone help?
Let me expand a bit on my comment with an example.
The On Error GoTo statement will take care of the IsError() part since the procedure will jump to the catch label, only if there's an error. Therefore, if we do jump into the error handler, then we definitely have an error.
A sample error handler:
Sub Whatever()
On Error GoTo catch
'do something
Leave:
Exit Sub
catch:
'If we hit this point, then we definitely have an error.
'At this point, we can query the error number if we want to take action based on the error.
If Err.Number = xxxx Then
Msgbox "Error " & xxxx
End If
Resume Leave
End Sub
Then, there's another approach if you want to suspend the error handler and then query if an error occurred.
On Error Resume Next
'do something
If Err.Number <> 0
'An error occurred
End If
Which we can then clear if we want to do this again later on on our method.
Err.Clear
Lastly, keep in mind Err is a global object so you don't need to create an instance. Further info on MSDN: Err object
I was having a problem of screen flickering when running a module. Then decided to used the method Application.Echo
However, I have notice that, using Application.Echo method without Error handling causes my screen to go blank if indeed an error occurs within the module.
As a result, I have thought of two approaches and would like to know which approach would be more efficient and if indeed these are the right ways of dealing with this kind of problem.
Approach 1:
Sub loopThrough()
On Error GoTo ErrorHandler
Me.Requery
Application.Echo False
'A for loop here........
Application.Echo True
exitErr:
Application.Echo True
Exit Sub
ErrorHandler: MsgBox Err.Description
GoTo exitErr
End Sub
Approach 2:
Sub loopThrough()
On Error Resume Next
Me.Requery
Application.Echo False
'A for loop here........
Application.Echo True
End Sub
Never go for approach 2, many things can go wrong here!
Take the following example code:
Sub loopThrough()
On Error Resume Next
Me.Requery
Application.Echo False
'Append new data to table
CurrentDb.Execute "INSERT INTO MyTable SELECT * FROM NewData", dbFailOnError
'Truncate the new data table, this data has been appended
CurrentDb.Execute "DELETE * FROM NewData"
Application.Echo True
End Sub
Say NewData contained an entry that could not fit into MyTable. That operation fails, we continue on, truncate NewData, poof, data gone without a trace.
Did an error occur? Not a clue, because we didn't get notified of an error probably not? Oh, wait, there's missing data! How did that happen?
If you take your first approach here, you:
Get an error message
Don't delete your data if there's an error
Have code that's a tiny bit lengthier
Do remind yourself not to call Application.Echo True twice, that's not necessary.
The usual structure in VBA for proper code is:
Public Sub SomeSub()
On Error GoTo ErrHandler
'Usual code here
ExitHandler:
'Perform operations needed when exiting, e.g. close open connections, set Application.Echo to true
'Then exit:
Exit Sub
ErrHandler:
'Report error here
MsgBox Err.Description
Resume ExitHandler
End Sub
Note that I use the Resume statement. That's specifically intended to jump out of an error handler. As far as I know it has no direct benefits outside of being syntactically clear, but I'm not 100% sure of that.
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
I've provided the actual code I'm using below.
The exact condition I'm trying to handle is the strCurrentRev argument as a zero-length string. (e.g. strCurrentRev="")
If I comment out the error handling statements, trying to execute the ASC method on a zero-length string throws Run-Time Error 5 for "invalid procedure call or argument".
If I then check err.Number it's = 5.
If I try to run the exact same statement with on error resume next active, it will not raise any errors, e.g. after execution err.number is always = 0.
If on error resume next is active, and you try to execute the ASC method from the immediate window (e.g. Type asc(strcurrentrev) and hit Enter) it will throw the run-time error and set the err.number property to 5.
I've never experienced this before. Why would having on error resume next active cause the error not to raise in normal execution???
Function NextRevLetter(strCurrentRev As String) As String
'This function returns the next revision letter given an existing revision letter.
'Declare variables
Dim lngX As Long
Dim strX As String
Dim strY As String
'First, check if we are dealing with rev A-Z or AA-ZZ
If Len(strCurrentRev) <= 1 Then
'Check that we can work with revision letter ***THIS IS WHERE I AM HAVING A PROBLEM!***
On Error Resume Next
Err.Clear
'Procedure call to flag errors with ASC method without changing any values
lngX=Asc(strCurrentRev)
lngX=0
On Error GoTo 0
If Err.Number > 0 Then
Err.Clear
If Len(strCurrentRev) < 1 Then
'No revision specified, assign first revision"
strCurrentRev = "-"
Else
MsgBox "The revision letter specified is not compliant. The next revision letter cannot be determined.", vbOKOnly, "Error: Revision does not follow rules"
'Return the existing revision (no change) and exit function
NextRevLetter = strCurrentRev
Exit Function
End If
End If
'Code continues - not important for this question...
Exit Function
You're not using the right tool for the job. Runtime errors should be handled, not shoved under the carpet (because that's what On Error Resume Next does - execution happily continues as if nothing happened).
You need to try to avoid raising that error in the first place. What's causing it?
lngX=Asc(strCurrentRev)
You already know what's happening:
The exact condition I'm trying to handle is the strCurrentRev argument as a zero-length string.
Well then, the correct way to handle this is to verify the length of strCurrentRev before you pass it to the Asc function, which you know will raise a runtime error #5 if you give it an empty string!
If strCurrentRev <> vbNullString Then
'calling Asc(strCurrentRev) here will not fail!
End If
I was asked to elaborate on a better way to handle the error, and this is the easiest place to do so. I think it's okay, because in a way it does answer the original question as well. However, let me say first that the right thing to do here is to avoid the error entirely, but for the sake of completeness, there is a way to do this cleanly with an error handler.
The idea is to check the error number, handle it by fixing the value, and then resuming the next line of code.
Function NextRevLetter(strCurrentRev As String) As String
'This function returns the next revision letter given an existing revision letter.
On Error GoTo ErrHandler
'Declare variables
Dim lngX As Long
Dim strX As String
Dim strY As String
'First, check if we are dealing with rev A-Z or AA-ZZ
If Len(strCurrentRev) <= 1 Then
'Procedure call to flag errors with ASC method without changing any values
lngX = Asc(strCurrentRev)
lngX = 0
'Code continues - not important for this question...
End If
Exit Function
ErrHandler:
If Err.Number = 5 Then
lngX = 0
If Len(strCurrentRev) < 1 Then
'No revision specified, assign first revision"
strCurrentRev = "-"
Resume Next
Else
MsgBox "The revision letter specified is not compliant. The next revision letter cannot be determined.", vbOKOnly, "Error: Revision does not follow rules"
'Return the existing revision (no change) and exit function
NextRevLetter = strCurrentRev
Exit Function
End If
Else If Err.Number = someOtherExpectedError
'handle it appropriately
Else
' !!! This is important.
' If we come across an error we don't know how to handle, we re-raise it.
Err.Raise Err.Number, Err.Source, Err.Description
End If
End Function
Note that the flow of your program is not interrupted by all of this error handling and we only handle the error that we're expecting. So, if an error is raised, we recover only if we know how to. Otherwise, execution is halted.
I would still prefer just to check to see if the value is = vbNullString though.
I just figured this out. The On Error GoTo 0 statement resets the Err.Number property to 0.
Sorry for wasting anyones time!!!!
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