HiveBrain v1.2.0
Get Started
← Back to all entries
debugMinor

Managing a programmatically accessible stack trace

Submitted by: @import:stackexchange-codereview··
0
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 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 Sub


Running 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 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 Function


And 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 Property


The 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 Function
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 Property

Context

StackExchange Code Review Q#135926, answer score: 2

Revisions (0)

No revisions yet.