Handling only specific VBA Errors - excel

I have two errors that are possible when running my code. The first is a common error in which my .Find method can't find anything, and I'd like it to resume next if this happens. It's a completely normal occurrence, and I need to leave it in for my manager to approve the code (legacy VBA code is still used and he's scared to change it.
I'd like to specify that if this error is seen then to do nothing, but if it's a specific other error to flag it and be handled by a more robust error handling.
The error I'd like to "ignore" (as in Resume Next or GoTo a specific place in the rest of the code without worrying about the error, I'm not worried about further down the code) is Runtime Error 91. Specifically in the code:
toFindCell1 = Cells.Find(nameVar).Row
where nameVar changes based on a for statement going down a list. I plan to then check it against existing information and use that variable to determine whether or not it exists. If it doesn't, then it will add it.
How can I specify the error I want to handle in VBA?

toFindCell1 = Cells.Find(nameVar).Row
Range.Find returns Nothing, the .Row member call isn't legal. Don't do it!
If your code doesn't throw error 91 in the first place, then you don't need to handle error 91.
Dim result As Range
Set result = Cells.Find(nameVar)
If Not result Is Nothing Then
toFindCell1 = result.Row
'....
Else
'not found.
End If

The best practice is indeed to use the If Not result Is Nothing Then, as mentioned in the answer of Mathieu.
However, by some specific cases it could be really a good idea to catch a specific error number and continue, by fixing it. This is definitely not one of them, but is a good illustration, of how to "play" with Err.Number:
Sub TestMe()
On Error GoTo TestMe_Error
Dim result As Range
Set result = Cells.Find("Something")
Debug.Print result.Row
Debug.Print "Something here"
Debug.Print 5 / 0
Debug.Print "This line is unreachable."
TestMe_Error:
Select Case Err.Number
Case 91
Debug.Print "ERROR 91!"
Err.Clear
Set result = Range("A100")
Resume
Case Else
Debug.Print "Some other error!"
End Select
End Sub
What is happening in the code above? On line Debug.Print result.Row it throws error 91, which is caught by the error handler and then cleared with Err.Clear. As this is expected, Set result = Range("A100") is used in the error handler and the code continues from the line, which threw the error, but this time, result is valid. Once, it reaches the new error 5/0, then it throws error, different than 91 and exits.
This is the output:
ERROR 91!
100
Something here
Some other error!
Keep in mind that using conditional error handling could be considered spaghetti code by plenty of devs.

Related

VBA function with "On Error GoTo error" always fall on error

Can we use : On Error GoTo ... in a VBA function ?
I always use it to manage my eventual error inside my Sub but when I use it inside a function it's like it didn't work as expected and always go through. When I remove it the function work well.
There is a specific way to manage error in function ?
EDIT
Here is an exemple of one of my function :
Function Row_Nbr_In_Sheet(shtName As String) As Integer
On Error GoTo ErrorManager
Row_Nbr_In_Sheet = ThisWorkbook.Worksheets(shtName).UsedRange.Rows.Count
ErrorManager:
MsgBox("Something weird here")
End
End Function
this function work well without "GoTo" but it trigger the MsgBox when I use it. It's exactly the same for each functions of my code.

How should VBA.CVErr() be used if it cannot support the Long vartype of vbObjectError..?

I'm in Access 2007 VBA, trying to return an #ERROR value from a function, as shown in the code below. But I've just discovered the largest number which VBA.CVErr(expression) will accept is 2^15-1, aka an Integer vartype; not a Long.
This seems incomprehensible, since the VBA constant vbObjectError is a Long. Other error functions work with longs; for instance: VBA.Error(vbObjectError) works fine.
In light of this issue, what suggestions are there to properly make use of vbObjectError to return user-defined errors as error objects from user-defined functions..?
Public Sub TesUDE()
Dim v As Variant
v = UDE()
Debug.Print TypeName(v), VBA.CStr(v)
End Sub
Public Function UDE() As Variant
On Error GoTo ErrorHandler
err.Raise 2 ^ 15 - 1 , , "This is a user-defined error." 'Works.
err.Raise 2 ^ 15 , , "This is a user-defined error." 'Overflow.
err.Raise vbObjectError, , "This is a user-defined error." 'It laughed at me.
ErrorHandler:
UDE = VBA.CVErr(err.Number)
End Function
The vbObjectError constant is useful to ensure your custom error numbers never colliding with a "built-in" error number, which makes error handling more robust in a way: it ensures error e.g. #91 consistently means "object reference not set", for example.
It implies custom errors are thrown/raised and handled, though - not returned.
Don't get me wrong: returning an Error-type value does have legitimate uses; like when you're writing a user-defined worksheet function and need Excel to distinguish between e.g. "an invalid reference was provided" (#REF!), "no match was found for the specified value" (#N/A), or "I've no idea what you're talking about" (#NAME?); in the Excel type library each of these errors have a corresponding XlErrXxxxx global constant defined, with an underlying value in the low 2000's.
It's entirely possible there's a similar use case in Access (I'm not all that familiar with Access), meaning the caller receiving the error is the Access query engine, much like the caller of a UDF in Excel is Excel's calculation engine.
Otherwise (i.e. if the caller is other VBA code), returning an error amounts to using the Error type for flow control, and making things return Variant meaning "this function might return a meaningful value of some type, or some error, maybe"... generally makes the code harder to read/follow.
So in use cases where the caller isn't your own VBA code, for error codes you mean to return as Error-type values (which is much cleaner than returning some magic non-zero Long number with the same meaning), you will want to skip the vbObjectError part.
Think of vbObjectError errors as "internal errors" that your VBA code handles, and Error/CVErr errors as "user-facing errors" that your VBA code returns. As come sort of self-inflicted convention =)
If the error you mean to expose is an actual custom VBA error code that you handle elsewhere in your VBA project, you'll want to "map" it to a finite-set of "user-facing" error codes - probably by defining constants, or enums for them:
Private Const ERR_CUSTOM_ERROR_1 = vbObjectError + 42
Public Enum UserFacingError
ErrFooWasNotBarred = &H7E1
ErrSomething
ErrSomethingElse
End Enum
'...
Public Function DoSomething(ByVal foo As Long) As Variant
On Error GoTo ErrHandler
'..."happy path"...
Exit Function
ErrHandler:
Select Case Err.Number
Case 5 'Invalid procedure call/argument
DoSomething = CVErr(ErrFooWasNotBarred)
Case ERR_CUSTOM_ERROR_1
DoSomething = CVErr(ErrSomething)
Case Else
DoSomething = CVErr(ErrSomethingElse)
End Select
End Function

Excel VBA On Error error

I am rather new at programming, and while learning Python also started experimenting with Excel VBA. I have an issue with the last one.
I have some large Excel sheets and tried to validate that data in specific columns matches data on another sheet in certain columns as they will be supposed to relate to each other by these values (and will be connected by a third value). To make this a bit more difficult, both of these columns may contain more than one value separated by "|". So, I have split these values in a list and I try to iterate through them to make sure all these values are set correctly, the connection will work fine.
All is fine as long as all is fine :) I have however an issue where there are two values in one of those columns and only one in the other. I would like this discrepancy to be noted on a sheet and then proceed to the next item.
The way that seemed to be applicable for me is to use "On Error GoTo ErrHandler", then note error on another sheet, and then user Resume to proceed.
Here is what I came up with:
For h = 0 To UBound(Split1())
For j = 1 To GetMaxRow("SpecificSheet", A)
On Error GoTo ErrHandler:
If Sheets("SpecificSheet").Cells(j, 1).Value = Split1(h) And Sheets("SpecificSheet").Cells(j, 2).Value = Split2(h) Then
DependencyOk = DependencyOk + 1
End If
Next j
Next h
ErrProceed:
Also ErrHandler is:
ErrHandler:
Sheets("Issues").Cells(x, 1) = "IssueDescription"
GoTo ErrProceed
It stops at line 2 with Subscript out of range for Split2(h) rather than moving on to ErrHandler and then ErrProceed. I have the feeling this must be something very obvious but I am just unable to get this working, and I am not able to find other way (like a try/except) in Excel VBA.
UPDATE:
Trying to clarify things a bit. The root of the issue is, that the Split2 list is shorter than Split1 - which is an issue with the input data and I'd like to capture this. I get the Split values from cells, where the values are separated by "|" characters:
CellValue = Sheets("SomeSheet").Cells(RowNumber, ColumNumber)
CellValueSplit() = Split(CellValue, "|")
And then iterate as:
For h = 0 To UBound(Split1())
So as Split1 moves on to the for example 3rd value, Split2 throws error and script stops. The best I was able to do so far was, that I let it proceed with the loop, but as this is a rather large sheet, it will fill the same error report ca. 200k times in this case, which I'd like to avoid. So I'd prefer it to proceed from after this loop once it hits out of range error, and proceed examining the next value.
Thank you for your help so far and in advance!
You have an issue with your syntax. The proper Error statement syntax is:
On Error GoTo <string>
On Error Resume Next
On Error GoTo 0
When using On Error GoTo <string> there is no ":" at the end. The ":" doesn't come into play until you create the target location. Example:
On Error GoTo Here
'// ---- Do something ---- //
Here:
'// ---- Handle the error ---- //
If you use On Error Resume Next, then you're telling the machine to ignore errors and proceed on to the next line of code.
When you useOn Error Return To 0, VBA will reset its error handling back to default. It's a good habit when using On Error Resume Next to insert On Error Return To 0 as soon as you no longer need it. On Error Resume Next has a real potential to break your code and make it behave strangely. Not to mention debugging can be a real nightmare. Check out the VBA manual from Microsoft for a more detailed explanation.
Finally, if your question is answered, you should mark it as answered.
vba-excelvbaexcel
The short and quick version is that VBA Error Handling Routine's only handle errors in the actual code execution, they do not fire when conditions expressed by the code are not met.
In your case, you do not need any error handling at all. In most cases it is actually best to avoid On Error GoTo .... There are cases where it's inevitable, but they are rare.
Try this IF THEN ELSE block:
If Sheets("SpecificSheet").Cells(j, 1).Value = Split1(h) And Sheets("SpecificSheet").Cells(j, 2).Value = Split2(h) Then
DependencyOk = DependencyOk + 1
Else
Sheets("Issues").Cells(x, 1) = "IssueDescription"
End If
Actually I have just found the issue. It was caused by a ":" left after an If statement a few rows earlier. I still don't really understand what it did, but I suggest not to reproduce it :)

VBA that finds duplicate information hitting Run-time error 91

Trying to use a form to create a row that stringes together two values in a concat using a string. Upon clicking the finish button the macro checks several values to determine if it can place in the information. One of these rules I am trying to set is detecting if the string/value already exists.
ID = txtStory.Value & "." & txtTask.Value
If Range("A7:A98").Cells.Find(what:=ID, LookAt:=xlWhole) > 0 Then
MsgBox "Story ID already exists.", vbExclamation, "Duplicate Found"
Exit Sub
End If
The code operates correctly when the information violates the rule. However, if the result is false (the values are not duplicate with anything in the column) then I receive "Run-time error '91': Object variable or With block variable not set"
What needs to be adjusted to fix the issue?
Find returns a Range object reference. When nothing matches the criteria, the function returns Nothing - a null reference.
This is indeed very very close to the linked would-be-duplicate, with the following nuance - this:
Range("...").Cells.Find(...) > 0
Is really doing this:
Range("...").Cells.Find(...).Value > 0
You're implicitly calling into the Range object's default member, which points to its Value.
It's that implicit member call that's throwing runtime error 91, because Find returned Nothing so you have no object to get a value from, to perform the > 0 comparison.
The solution for the error 91 is, as in the linked Q&A, to first verify that Find returns a valid object reference.
The solution to avoid similar bugs in the future and in many other circumstances, is to avoid implicit default member calls - i.e., write code that means what it says and that says what it means.
Set result = Range("...").Find(...)
If Not result Is Nothing Then
'.Find call was successful
If result.Value > 0 Then '<< explicit Range.Value member call
'...
End If
Else
'.Find call failed
End If
Note that the .Cell member call is redundant.

Debugging errors in VBA classes in Excel

I have a module with a function similar to this:
MainModule
Sub Test()
On Error Resume Next
Dim O1 As New Class1
O1.DoSomething
On Error GoTo 0
End Sub
and a few classes similar to this:
Class1
Sub DoSomething()
FindStuff
'create similar objects who perform similar operations and raise similar errors
Dim O2 As New Class2
O2.DoSomething
End Sub
Function FindStuff() As Stuff
'scan the WorkBook, the file system, etc. and organize the members of the object
If CorruptedFileSystem Then Err.Raise 514, "File system corrupted"
If CorruptedWorkBook Then Err.Raise 515, "WorkBook corrupted"
If Found Then Set FindStuff = FoundStuff
End Function
If I set the error trapping to Break in Class Module then the On Error Resume Next will be ignored and every Err.Raise will stop the execution inside the class.
If I set the error trapping to Break on Unhandled Errors then the Err.Raise will stop the execution at the call on the main module, not inside the class.
So in one case I can't execute the code with handled errors, in the other case I can't debug unhandled errors.
The problem becomes unmanageable when the project grows and the main module creates an object that opens a form (which is another object) that creates more objects. Some of the methods handle their own errors and some are designed to abort and raise the error to be managed by the caller.
Is there a way to handle and debug errors in classes?
EDIT
Apparently my question wasn't clear enough. I changed the title and I will try with a clearer example.
Module1
Sub Test1()
Dim O As New Class1
O.UnhandledCall
End Sub
Sub Test2()
On Error Resume Next
Debug.Print 1 / 0
Dim O As New Class1
O.HandledCall
On Error GoTo 0
End Sub
Class1
Sub UnhandledCall()
Debug.Print 2 / 0
End Sub
Sub HandledCall()
Debug.Print 3 / 0
End Sub
Test1
Set Error Trapping = Break on Unhandled Errors and execute Test1. The debugger will not stop at the unhandled error 2 / 0. Instead it will stop at O.UnhandledCall, making it impossible to know what line caused the error, what were the local variable values, the stack, etc.
Test2
Set Error Trapping = Break in Class Module and execute Test2. The debugger will not stop at 1 / 0, good, because the error is handled. But it will stop at 3 / 0 inside the class even if the error is handled inside the caller function, at the same level as 1 / 0.
Sad summary
So with the first setting I can't see where en error is trhown, with the second setting I cant run a macro that cleanly handles errors.
This is obviously an oversimplified example. The real world case I'm dealing with at this moment is a form that creates dozens of objects, some objects check some text files, other objects open drawings on a CAD via COM, other objects talk to a database, etc. If any of the conditions is inconsistent I want to abort the form opening.
As the objects are created, they execute thousands of lines of code, with hundreds of managed errors. When they find something unmanageable in a file, in a drawing or in a database, they defer the error handling to their caller, climbing the stack up to the form that should fail to open and up to the caller that should detect the error and do something about it.
I would expect the debugger to run smoothly through the managed errors and stop when there is an unmanaged error at the offending line. Instead the debugger works as expected in modules, but in classes it either stops at all the error or it never stops, regardless of whether they are managed or not.
For example if I set Error Trapping = Break in Class Module all the managed errors will break the execution as in Test2, and my debugging session will never end.
While if I set Error Trapping = Break on Unhandled Errors then I will never know what triggered the error, because the debugger will climb through all the classes up to the first object and tell me that that's the line that caused the error as in Test1.
As you've noticed, you can't bubble up runtime errors raised in a class module and debug on-the-spot just by tweaking the IDE/debugger settings.
There's another way though. Define a project-wide conditional compilation value, say DEBUG_MODE:
In your class modules' error handlers, use conditional compilation logic to make a programmatic break:
Public Function FetchResults(ByVal filter As String) As Collection
On Error GoTo CleanFail
Dim results As Collection
Set results = this.Repository.Where(filter)
CleanExit:
Set FetchResults = results
Exit Function
CleanFail:
#If DEBUG_MODE = 1 Then
Stop
#Else
Err.Raise Err.Number 'rethrows with same source and description
#End If
Set results = Nothing
Resume CleanExit
End Sub
If you don't mind the VBE popping up on your puzzled users then you could also use Debug.Assert statements to break execution when a condition is not met:
Public Function FetchResults(ByVal filter As String) As Collection
On Error GoTo CleanFail
Dim results As Collection
Set results = this.Repository.Where(filter)
CleanExit:
Set FetchResults = results
Exit Function
CleanFail:
Debug.Assert Err.Number <> 0 ' will definitely break here
Set results = Nothing
Resume CleanExit
End Sub

Resources