Debugging errors in VBA classes in Excel - 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

Related

How do I Late Bind a Class Module from my Project?

I have two class modules in my project, "ClassAlfa" and "ClassBeta". I have separated the code between the two classes so that if an end-user does not have ClassBeta included in their project, then any of ClassAlfa's late bound references to it gracefully fail. Where I'm having trouble is making the script work if the end-user DOES have ClassBeta available.
Within ClassAlfa, I have the following code:
Option Explicit
Public myClass as Object
Private Sub Class_Initialize()
On Error Resume Next
Set myClass = Application.Run("ClassBeta")
If Err = 0 Then 'End user has this class available, go ahead and use it.
'Do code with ClassBeta
Else
'Do code without ClassBeta
End If
On Error GoTo 0
End Sub
This throws the following error on the Set line:
Run-time error '1004':
Cannot run the macro 'ClassBeta'. The macro may not be available in this workbook or all macros may be disabled.
I have also tried replacing the Set line with this:
Set myClass = CreateObject("ClassBeta")
which instead throws the error
Run-time error '429':
ActiveX component can't create object
Also does not work:
Set myClass = CreateObject("'" & ThisWorkbook.Name & "'!" & "ClassBeta")
What is the proper way to late bind a custom class from my own project?
There is no mechanism in VBA that would allow you to check if a class exists by it's name and to create a new instance if it is.
However, it is possible to achieve. Let's dissect the problems and see how we can work around them.
Problem 1
You need to create an instance of a class that you are not sure it exists in the current project. How?
As you already tried, Application.Run and CreateObject do not work.
Application.Run is only capable of running methods in standard .bas modules (not class modules). It does not create instances of classes.
CreateObject does a few things behind the scenes. First, it calls CLSIDFromProgIDEx using the ProgID (the string) you are passing. Then, it calls CoCreateInstance. The problem is that VBA classes do not have their ProgIDs in the registry so CreateObject simply doesn't work. You would need to have your class in a registered .dll file of ActiveX.exe to make this work.
That leaves us with the New keyword to instantiate a new ClassBeta. That obviously works when the class is present but gives a 'User-defined type not defined' compiler error when it's not. There are only 2 ways to supress this compiler error:
Have a compiler directive
Not have the New ClassBeta at all
A compiler directive would look like this in ClassAlfa:
Option Explicit
#Const BETA_EXISTS = False
Sub Test()
Dim myBeta As Object
#If BETA_EXISTS Then
Set myBeta = New ClassAlpha
#End If
Debug.Print "Type of 'myBeta' is: " & TypeName(myBeta)
End Sub
This compiles fine without having the ClassBeta in the project but you could never make the BETA_EXISTS conditional compiler constant to turn True because:
Only conditional compiler constants and literals can be used in expression
So, the last option is not to have the New ClassBeta anywhere in the project. Except, we do. We can put it in the ClassBeta itself as a factory:
Option Explicit
Public Function Factory() As ClassBeta
Set Factory = New ClassBeta
End Function
When the class is missing, the factory is missing and the New ClassBeta will not throw a compiler error.
Problem 2
How do we call the .Factory method on the ClassBeta?
Well, we obviously cannot create a new instance of ClassBeta because that is our first problem.
What we can do is to make sure that ClassBeta always has a default global instance (like userforms have). To do that we need to export the class to a .cls text file and edit it with a text editor (like Notepad). The text should look like this:
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "ClassBeta"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = True
Attribute VB_Exposed = False
Option Explicit
Public Function Factory() As ClassBeta
Set Factory = New ClassBeta
End Function
Notice that I've changed the VB_PredeclaredId attribute to True (manually, in the text editor). Now we can import the class back. We can check if it worked by typing ?Typename(ClassBeta.Factory) in the Immediate window and then pressing Enter. We should see:
The code in ClassAlfa can now be written as:
Option Explicit
Sub Test()
Dim myBeta As Object
On Error Resume Next
Set myBeta = ClassBeta.Factory
On Error GoTo 0
Debug.Print "Type of 'myBeta' is: " & TypeName(myBeta)
End Sub
Problem 3
If we remove ClassBeta from the project, the following line does not compile:
Set myBeta = ClassBeta.Factory
But, compared to problem 1, this time the compiler error is 'Variable not defined'.
The only way that I could think of, to get rid of this new compiler error, is to turn off Option Explicit. Yeah! That bad!
ClassAlfa:
'Option Explicit 'needs to be off to be able to compile without the ClassBeta class
Sub Test()
Dim myBeta As Object
On Error Resume Next
Set myBeta = ClassBeta.Factory
On Error GoTo 0
Debug.Print "Type of 'myBeta' is: " & TypeName(myBeta)
End Sub
Final Thoughts
You could do the development while Option Explicit is on and turn it off for your users.
If your users will use the ClassAlfa with Option Explicit off, the the code should work fine. But if they want ClassBeta as well, then the only way they could get it would be via importing the .cls file. Copy-pasting code won't work because they would lose the global instance of ClassBeta (the one we set in the VB_PredeclaredId hidden attribute).
I must say that I do not recommend removing Option Explicit because that could lead to other issues. I do not think there is a way to achieve what you want in a 'clean' way.

Handling only specific VBA Errors

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.

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

VBA runs fine upon button click, but throws error in the end

I have a complex VBA function that makes lots of calculations, opens a template and inserts numbers and charts, all in perfect formatting.
The function can be represented as:
Function Simular()
Dim wbout As Workbook
Set wbout = Workbooks.Open(ThisWorkbook.Path & "\out.xltx")
' lots of processing and outputting
Set Simular = wbout
End Function
The spreadsheet has a button that calls Simular().
I'm astounded that, sometimes, when all processing is done, I get the famous dialog:
Object doesn't support this property or method
I tried debugging with breakpoints. The error never shows up -- well, except when I press F8 on the End Function line, which baffles me.
What could be wrong?
The problem is that the button I used called a Function directly. It seems this is inappropriate and buttons should only call Subs.
I did some tests and it seems behavior when calling a Function from a button varies (from other "places" too). This is the code I used:
Function AsVariantNoSet()
End Function
Function AsVariantSet() ' This represents my original case
Set AsVariantSet = ThisWorkbook
End Function
Function AsChartNoSet() As Chart
End Function
Function AsChartSetWorkbook() As Chart
Set AsChartSetWorkbook = ThisWorkbook
End Function
This is the result:
From Button From Code
----------- ---------
AsVariantNoSet() no error no error
AsVariantSet() error A no error
AsObjectNoSet() error B no error
AsObjectSet() error A no error
AsObjectSetWrong() error C error C
error key:
A: Object doesn't support this property or method [OK/Help]
B: System Error &H80070057 (-2147024809). The parameter is incorrect. [OK/Help]
C: Run-time error '13': Type mismatch [End/Debug/Help]

OLE Excel object manipulation causes run-time error '91'

I am maintaining an application that was written in Visual Basic 6.0 and makes use of the several OLE controls with Excel.Sheet.8 class objects. Several users are getting the following error when they reach a point in code that attempts to manipulate the excel objects.
Run-time error '91': Object variable or With block variable not set
Below are examples of the code that trigger this error. I believe that the issue happens at:
Set oExcel = oleXl.object
Here are the points in the code where it happens:
Private Sub Form_Load()
Dim i As Integer
Dim j As Integer
Dim sTempStringA As String
Dim sTempStringB As String
'Set up excel sheet
centerform Me
Set oOutGrid = oleXlOutput.object
...
Private Sub Form_Load()
centerform Me
Set oOtherFx = oleXlFx.object
...
Private Sub Form_Load()
Dim iRet As Integer
Dim i As Integer
On Error GoTo Err_Handler
centerform Me
Call InitArray
Me.Caption = "TJUJ | Version " & version & " | Enter Custom Fx"
Set oBook = oleExcel.object
...
Is there a specific situation or environment in which this error would be generated from this line of code OR a way that I can ensure the object will always be accessible at this point in the code?
The error only happens occasionally, and I can't reproduce it on my developer machine at all. I also do not have access to the machines that it is happening on, but it seems to be encountered when there is an instance of the EXCEL.EXE process running.
When you get runtime-error 91, you can bet there's an uninitialized object somewhere in the statement. In other words, you are trying to use the properties or methods of a variable/object with a value of Nothing.
In your examples, oleXl, oleXlFx, and oleExcel are probably Nothing. So when you refer to their .object property, you trigger the RTE.
Somewhere in your code these variables have to be initialized to something. Look for statements like Set oleXl = CreateObject("Excel.Application") or Set oleXl = New Excel.Application
One suggestion; when you find the statements that actually initialize those OLE objects, check to see how the error-handling is coded. If you see things like this:
On Error Resume Next
Set oleXl = CreateObject(...
add a test to make sure the object was instantiated
On Error Resume Next
Set oleXl = CreateObject(...
If oleXl Is Nothing Then
MsgBox "Hey, my object is Nothing!"
End If
Microsoft suggests that we can fix error 91 by creating a new registry key. To create a new key follow the steps below.
Click on the Windows Start menu
Type Regedit in the search box
Press Enter
Locate the following entry in the registry. HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Transaction Server
Now select the transaction server and right click on it
Select New and then choose Key
Name the key as Debug
Right click on the Debug key and choose New
Now select Key and name the key as RunWithoutContext
Ref: http://backspacetab.com/error-91/

Resources