debugMinor
Managing a programmatically accessible stack trace
Viewed 0 times
managingstackaccessibletraceprogrammatically
Problem
VBA has a call stack... but there's no programmatic way to tap into it, which means in order to get a stack trace for a runtime error, one has to manage it manually.
Here's some example code that demonstrates a custom
Running
The value of this isn't so much the stack trace itself (after all the VBE's debugger has a call stack debug window), but the ability to log runtime errors along with that precious stack trace.
Here's the
```
VERSION 1.0 CLASS
BEGIN
MultiUs
Here's some example code that demonstrates a custom
CallStack class in action:Option Explicit
Private Const ModuleName As String = "Module1"
Sub DoSomething(ByVal value1 As Integer, ByVal value2 As Integer, ByVal value3 As String)
CallStack.Push ModuleName, "DoSomething", value1, value2, value3
TestSomethingElse value1
CallStack.Pop
End Sub
Private Sub TestSomethingElse(ByVal value1 As Integer)
CallStack.Push ModuleName, "TestSomethingElse", value1
On Error GoTo CleanFail
Debug.Print value1 / 0
CleanExit:
CallStack.Pop
Exit Sub
CleanFail:
PrintErrorInfo
Resume CleanExit
End Sub
Public Sub PrintErrorInfo()
Debug.Print "Runtime error " & Err.Number & ": " & Err.Description & vbNewLine & CallStack.ToString
End SubRunning
DoSomething 42, 12, "test" produces the following output:Runtime error 11: Division by zero
at Module1.TestSomethingElse({Integer:42})
at Module1.DoSomething({Integer:42},{Integer:12},{String:"test"})
The value of this isn't so much the stack trace itself (after all the VBE's debugger has a call stack debug window), but the ability to log runtime errors along with that precious stack trace.
Here's the
CallStack class - note that I opted to set its VB_PredeclaredId attribute to True so that it could be used as a globally-scoped CallStack object (similar to a C# static class). I chose to work off a Collection for simplicity, and because I didn't mind the performance penalty of using a For loop to iterate its items in reverse. I did consider using an array instead, but it seemed the boundary handling and constant resizing left a sour taste to the code: I deliberately preferred the readability and simplicity of a Collection over the For-loop performance of an array.```
VERSION 1.0 CLASS
BEGIN
MultiUs
Solution
The
Would feel less cluttered and easier to read as:
Therefore, I'd implement it simply as such:
And then let the client's error-handling code
Let's go wild here. The range of valid values for an
The only unsigned integer type in VBA is
The
IStackFrame_ToString implementation is overkill. While the parameter types and values are extremely useful in specific error-handling scenarios, outputting them as standard part of the stack trace doesn't look right:Runtime error 11: Division by zero
at Module1.TestSomethingElse({Integer:42})
at Module1.DoSomething({Integer:42},{Integer:12},{String:"test"})Would feel less cluttered and easier to read as:
Runtime error 11: Division by zero
at Module1.TestSomethingElse
at Module1.DoSomething
Therefore, I'd implement it simply as such:
Private Function IStackFrame_ToString() As String
IStackFrame_ToString = this.ModuleName & "." & this.MemberName
End FunctionAnd then let the client's error-handling code
Peek at the stack trace and output/log parameter values when they are deemed relevant. After all, the pointer address of an object isn't really useful beyond "is it 0 or anything else" (ObjPtr(Nothing) returns 0, which is indeed useful when you're up against an object reference not set runtime error 91) - the actual address in itself is... meaningless junk, especially since these values are pretty much single-use (e.g. after executing Set foo = New Bar, the value returned by ObjPtr(foo) will be different at every execution).Let's go wild here. The range of valid values for an
Integer is -32,768 to 32,767. I can't imagine a procedure taking -12 arguments, and I'm not sure one with over 255 arguments would even compile - so Integer is definitely overkill for the index of ParameterValue:Public Property Get ParameterValue(ByVal index As Integer) As Variant
Attribute ModuleName.VB_Description = "Gets the value of the parameter at the specified index."
ParameterValue = this.values(index)
End PropertyThe only unsigned integer type in VBA is
Byte, ranging from 0 to 255; it also happens to be the smallest available integer type. I'd most probably want to strangle whoever wrote a procedure taking 255 arguments, and I'm not sure why but if there's a limit to the number of arguments that a VBA procedure can take, 255 seems a likely possible number. So Integer could be harmlessly replaced with Byte wherever it's used to iterate parameters (e.g. in Create) or access them (e.g. ParameterValue).The
values collection will be able to hold more than that though, so there should be some code to validate the inputs and trap a runtime error in CallStack.Push... because you definitely don't want your call stack to be the source of an error!Code Snippets
Runtime error 11: Division by zero
at Module1.TestSomethingElse({Integer:42})
at Module1.DoSomething({Integer:42},{Integer:12},{String:"test"})Private Function IStackFrame_ToString() As String
IStackFrame_ToString = this.ModuleName & "." & this.MemberName
End FunctionPublic Property Get ParameterValue(ByVal index As Integer) As Variant
Attribute ModuleName.VB_Description = "Gets the value of the parameter at the specified index."
ParameterValue = this.values(index)
End PropertyContext
StackExchange Code Review Q#135926, answer score: 2
Revisions (0)
No revisions yet.