Home
Foreword
Preface
Class Idioms
Collections
Implements
Constructors
Terminate
Forms
On Error
Frameworks
F.A.Q.
Value-added
FSMs
Constants
GOTO
Hungarian
Nothing
Properties
Big EXEs

Part I
The Basics

There is nothing strange or mysterious about how errors work in Visual Basic: VB´s behaviour in error situations is well-defined and consistent, and it is only by understanding this behaviour that we can write reliable code. I´ll assume that since you´re reading this you already know about VB´s Err object and how it is used, but since some aspects of Visual Basic´s error mechanism are often poorly understood I´ll start by offering my own summary of the important points.

(1) Unhandled Errors Propagate Up the Call Stack

If you don´t trap an error with some kind of On Error statement you will go to Hell. Actually, this usually isn´t so, because if an error isn´t trapped in the originating function, that function (*) simply terminates and the error is re-raised in the calling function. The caveat is that sooner or later there is no calling function, because all sequences of function calls have to start somewhere. When handling errors it looks like we need to treat the places where chains of function calls can start as special cases.

(* NOTE: When I say `function´ I mean either a VB Function or Sub. This is because I used to be a C programmer, but it´s also easier to say than `function or sub´ each time.)

(2) Functions are Called from Event Handlers

Most sequences of function calls begin and end in an event handler. Event handlers are things like Form_Click or Class_Terminate, and also the handlers for any user-defined events we raise in our classes. The vast majority of Visual Basic code we write will be invoked directly or indirectly by an event handler (`indirectly´ in the sense that our code may be in a function that is called by a function that is called by an event handler). There are a couple of exceptions to this, notably Sub Main, which may run when our program first starts (`may´ because we don´t have to have a Sub Main). The other exception is a call-back method called through a late bound object reference (ie. the reference is defined As Object or As Form) - like user-defined events, the call stack shows such methods invoked by [<Non-Basic Code>].

The story so far: untrapped errors are re-raised in the calling function, but ultimately such errors will find their way back to an event handler, which has no calling function.

(3) Untrapped Errors in Event Handlers are Fatal

So what happens if our error propagates all the way back to an event handler but we still don´t have an On Error statement? This is the limiting case: the error has nowhere else to go, so the Visual Basic interpreter itself catches it. Unfortunately, Visual Basic´s way of dealing with such errors is to display a MessageBox and terminate the program. This is generally a Bad Thing. (There is one exception to this behaviour, which is the Initialize event of a Form or Class. For error propagation purposes, the line of code that creates the form or class instance behaves as if it were a direct call to the Initialize event handler, and so, uniquely, the error propagates out of the class.)

This brings us to the only non-negotiable rule of Visual Basic error handling: we MUST trap ALL errors in event handlers. More specifically, we must code an On Error statement for:

  • Every event handler in every form.
  • Every Class_Terminate event.
  • Sub Main, if we have one.

If we don´t do this, we run the risk that our program may terminate without warning. Of course we´re only talking about event handlers here; this doesn´t mean we have to code On Error statements in every function that we ever write.

[You will hear it suggested in some quarters that we must have error handlers in every single function we write. Although in many cases this will give us a more informative error report, often it will not, and, as we´ll see, it also mandates a huge amount of extra work. This advice seems peculiar to Visual Basic, as I´ve never heard it preached to, say, C++ programmers. The last word must go to Bjarne Stroustrup, inventor of C++, who has this to say in the Annotated C++ Reference Manual: `Exception handlers are assumed to be rare compared to function definitions...The aim is to allow programs to be constructed out of fault-tolerant subsystems without requiring attention to exception handling in every function. In other words it is not reasonable to make every function fault-tolerant.´]

Just a small clarification: we don´t have to provide error-traps for event handlers that we don´t write; for example, if we don´t want any code for our form´s KeyPress event, we don´t need to add a KeyPress event handler just so we can put an On Error statement in it.

(4) Visual Basic has an `Error In Progress´ Mode

When an error happens and we have an `On Error GoTo´ statement in our code, VB jumps to the label we specified and then enters a special `error in progress´ mode. In practice this means that all `On Error´ bets are off: when Visual Basic is in this mode, any further runtime error abandons the current function and re-raises the error in the calling function. This happens regardless of any On Error statement in the current function - including any `On Error´ statements inside the error handler. That last bit is very important: it´s tempting to write `On Error Resume Next´ as the first line of our error handler block, but VB will just ignore us.

There are several ways to leave VB´s error mode: Resume, Resume Next, Resume <label>, Exit Sub or Err.Raise. (Since version 4, if VB encounters an End Sub in an active error handler this has the same effect as Exit Sub.)

From the above we can infer the following three important points: (i) using a Goto statement to get out of an error block is wrong, (ii) we can´t nest error handlers, and hence (iii) we need to be very careful about what we do in an error handler block because any further runtime error will abort the current function. More about what we can and can´t do inside an error handler coming up.

(5) Any Code in an Error Handler is Unsafe

As we´ve seen, any code running inside an active error handler is effectively naked in the face of runtime errors. This means we must try to keep things simple. Point (3) tells us that provoking another runtime error inside an event handler´s error block is immediately fatal; doing so inside an ordinary function´s error block is merely confusing. The reason it´s confusing is that the details of the original error are lost, and a new error is raised in the calling routine. It is rarely useful to know the details of this second error.

Part II
Expecting the Unexpected

Now that we have the basics, let´s look at some of the error-trapping issues we need to concern ourselves with, and at how we might devise a general scheme for dealing with errors. There are many angles to error trapping, and we can´t cover them all here. Our main concern will be to construct a robust and consistent mechanism for reporting and recovering from errors.

The first thing we need to do is draw a distinction between errors we expect and ones we don´t. This is crucial, yet it´s a distinction that many programmers fail to make. It´s very common, for example, to find an error handling strategy that simply funnels all errors through a single function - the `error handler´. This, of course, is nonsense; an error is defined by the context in which it occurs, and attempting to build a context-independent error handling module is futile. On the other hand, a central error reporting module may well be a requirement, and we´ll see something like that as we progress.

The distinction between expected and unexpected errors is this: we can write specific code to react to errors that we anticipate, but we can do very little about unexpected errors. Since anticipated errors will usually be handled close to where they occur (often in the error handling block of the same function) they will not feature significantly in the mechanisms we´ll discuss here. Handling anticipated errors (or, more generally, exceptional cases) is simply an extension of our mainstream coding task. The following example shows how we might deal with an anticipated error.

Example Code
Handling anticipated errors

This is an extract from a form that deals with constructing an order. An order consists of a number of lines, and what we see here is abridged code from the `Add´ button event handler, which deals with adding a new line to the order. The code focuses on what happens when the user enters a product code that doesn´t exist on the database. He is given the opportunity to add this product to the database and then continue.

Private Sub cmdAdd_Click()

    On Error GoTo ErrorBlock

    Dim Orderable As COrderable
    Set Orderable = New COrderable

EditItem:

    If Orderable.Edit() = vbOk Then

        ' The user entered a product code and clicked OK.
        ' Now we need to look up the product description.
        ' The lookup throws an exception if it´s an unknown
        ' product code.

        Set Orderable = m_Database.Orderables(Orderable.Code)

        ' Here's where we'd have code to add the new item
        ' to the order.
        ...
    End If

ExitBlock:
    Exit Sub

ErrorBlock:

    If Err.Number = COrderable_eNotFound Then
        If MsgBox("Unknown product code. Add this product?", _
                  bYesNo + vbCritical, App.Title) = vbYes Then
            ' He said YES. Go off and create a new product.
            If AddNewCommodity(oOrderable) = vbCancel Then
                ' He cancelled the 'add new' operation, so go
                ' back to the initial edit dialog.
                Resume EditItem
            Else
                ' Successfully added the new product to the
                ' database. Now we can continue as if nothing
                ' happened.
                Resume
            End If
        Else
            ' He said NO. Just go around again, and so the
            ' user gets to cancel the edit dialog or change
            ' the product code he entered.
            Resume EditItem
        End If
    End If

End Sub

This code is a good illustration of an error that we can recover from completely. By successfully adding the unknown product code to the database we can go back and retry the original failed line with the expectation that it will now succeed (hence the Resume statement). Otherwise we must backtrack and force the user to try another code (Resume <label>).

The big problem with this code is what happens if a different runtime error happens - in fact the function just exits without doing anything, and the error goes unannounced. This is the part that will concern us for the rest of this chapter.

When considering what sort of things we could do when an unexpected error occurs, these are the main general strategies we can adopt:

(1) Ignore the error and just continue.
(2) Tell the user and let him to decide what to do.
(3) Abandon what we were doing and tell the user.

The obvious thing missing from our example error handler above is an `Else´ part to the `If´ statement: ie., what do we do if the error isn´t the specific one we´re looking for? Here´s how the error handler might change to implement each of these three strategies:

Strategy 1
Ignore Errors

ExitBlock:
    Exit Sub

ErrorBlock:

    If Err.Number = COrderable_eNotFound Then
        ` ...code to recover from this error
    Else
        Resume Next
    End If

End Sub

Ignoring errors is barely worth considering except in the special case of cleaning up after previous errors. If something went wrong, ignoring it and continuing more or less guarantees that something else will go wrong, and so on, leading to an endless cascade of errors.

Strategy 2
Ask the User

ExitBlock:
    Exit Sub

ErrorBlock:

    If Err.Number = COrderable_eNotFound Then
        ` ...code to recover from this error
    Else
        Select Case MsgBox( _
                      Err.Description, _
                      vbExclamation + vbAbortRetryIgnore, _
                      App.Title)
            Case vbAbort: Err.Raise Err.Number
            Case vbRetry: Resume
            Case vbIgnore: Resume Next
        End Select
    End If

End Sub

On the face of it this is a better strategy because it deals with the context issue by asking the user what to do. The problem with this is that 999 times out of 1000 the user won´t have the faintest idea. Don´t forget that he´s an administrator sitting in an office typing orders into our system, and we`re expecting him to react sensibly to a message like `Out of String Space: Abort, Retry, Ignore?´.

There are so many other things wrong with this dialog that we could write a book it (people have - Alan Cooper´s About Face is a good one), but we´ll concern ourselves with the most obvious problem: an unexpected error will, almost by definition, be something that the user isn´t qualified to make a decision about. The single mitigating factor in getting the user involved here is that we can tell him precisely where the error occurred. More about this later.

(We should note in passing that asking the user what to do isn´t always a bad thing. If we can work out from the context what the likely cause of the error is, can ask the user a question in terms of the domain he understands. Now, of course, we´re back to targeting specific errors, not unexpected ones.)

Strategy 3
Aborting the Function

ExitBlock:
    Exit Sub

ErrorBlock:

    If Err.Number = COrderable_eNotFound Then
        ` ...code to recover from this error
    Else
        Err.Raise Err.Number
    End If

End Sub

If we don´t know what happened, we can´t know how to deal with it. Hence if we attempt a function and something unexpected goes wrong, the best thing we can do is call the whole thing off and tell the user we´ve done so, possibly doing some clean-up actions along the way. Recording information about the source of the error is important (we´ll get to that later), although how much of it we present to the user is one of those GUI design issues we mentioned earlier.

To cut a long story short, we´ll dispense with (1) and (2) and adopt (3) as our chosen strategy. We can think about `aborting a function´ on two levels. The code shown here aborts the current Visual Basic Function or Sub by re-raising the error in the caller (look back at Part I if you don´t understand this), but we can also consider a `function´ in higher-level terms as an action kicked off by the user when, say, he clicked a button. In this sense, a `function´ is a discrete piece of work done by our program in response to some event, and most events are started by user-interface actions.

Of course, simply aborting the current VB function by re-raising the error in the caller is merely a different version of what we did in Strategy 1. We´re omitting more code than we did there, but essentially we´re doing the same thing: resuming execution at an undetermined point after failing to do some pre-requisite action(s). (If anything this is more likely to cause subsequent mayhem because we´re likely to have omitted more of these pre-requisite actions.) Again, recall that we´re talking about unexpected errors here - it´s quite valid to carefully design transaction-like structures in our code for specific errors.

If we think about functions in the second, higher-level sense, we reach a scenario where if anything unexpected goes wrong we want to abort all the way back to originating event handler. This is by far the safest thing to do, since we´d avoid compounding the problem by running more code and we´d only have a single error to report. For C/C++ programmers, the effect we´re looking for is equivalent to a longjmp back to a context we´d saved in the event handler with setjmp.

VB has no direct equivalent of setjmp/longjmp, but Err.Raise achieves the same effect if, for example, we omit error handlers in all of our functions (NOTE: that´s functions, not event handlers - as we´ve seen, omitting error handling code in event handlers is instantly fatal and hence not an option). We can also achieve the same effect using the Strategy 3 outlined above, since re-raising en error with Err.Raise is effectively the same as not handling the error at all.

As long as we have Err.Raise as the default error-handling action in every function that has an error handler, unexpected errors will abandon all processing right back to the top of the call sequence, which will always be an event handler (or Sub Main). We then have the option of reporting the error or simply ignoring it before exiting the event handler. Note that ignoring the error here isn´t the same as ignoring it in the Strategy 1 sense, since no further code will be executed. It´s probably not a good idea to ignore the error, of course, since obviously something has gone wrong. Whether we report the error to the user or simply log it behind the scenes is, once again, a design decision.

Using our chosen strategy (Strategy 3), here´s how the error handling block of every event handler might look:

ExitBlock:
    Exit Sub
ErrorBlock:
    MsgBox "Unexpected error " & CStr(Err.Number) _
           & ": " & Err.Description, vbExclamation, App.Title
    Resume ExitBlock
End Sub

In this example we´ve chosen to report the error to the user, but the important thing is that after doing so the event handler exits and nothing else happens until the next event.

Part III
Where Did We Come From?

So far we´ve seen two different ways to ensure that unexpected errors abort processing back to the top level: code a default Err.Raise in every error handler, or simply omit error handlers altogether. In practice our code will have a mixture of both approaches, since there will be errors we want to target specifically, but either way there´s one important thing missing from our error report: the location of the error. Since errors are highly dependent on context it´s clearly very useful (whether to the user or the developer) to know exactly where the error occurred.

Before we continue, we should consider how useful this information is going to be, and whether we're likely to need it during the development of our software or after the finished program is deployed. Visual Basic has a Break On All Errors mode built into the IDE, so during development we get the precise location of runtime errors for free. If we find that we routinely need such a mechanism after we've 'finished' the program, then maybe this is a signal that we're approaching the design and construction tasks less rigorously than we should be.

With this caution in mind, we´ll look at two approaches to tracking the source of an error.

Method 1 (Low Tech)

There are two kinds of information we´d like to know when an error is reported: the location of the error and the sequence of function calls we made up to the point where the error happened. The former is simple enough, as all we need to do is save the function name and module name when the error occurs. We can automate this by replacing Err.Raise in our err-trapping scheme with a function call. The function will look something like this:

Public Sub Throw(ByVal ModName As String, _
                 ByVal FuncName As String)
    If m_ErrDetails.Number = 0 Then
        m_ErrDetails.Module = ModName
        m_ErrDetails.Function = FuncName
        m_ErrDetails.Number = Err.Number
        m_ErrDetails.Description = Err.Description
        ` ...etc.
    EndIf
    Err.Raise Err.Number
End Sub

Note that we´re saving the error details in a private data structure and that the details only get saved when the error is first raised. When we get back to the event handler at the top of the call stack, our reporting function will report the contents of this structure.

This is probably the simplest scheme we can get away with, but it doesn´t help us with tracing the route by which we got to the error. There are a number of ways to trace the route, but the most elegant is to build it up in reverse as we pop back up through the various error handlers. A better version of our Throw function that incorporates this idea is:

Public Sub Throw(ByVal ModName As String, _
                 ByVal FuncName As String)
    If m_ErrDetails.Number = 0 Then
        m_ErrDetails.Number = Err.Number
        m_ErrDetails.Description = Err.Description
        ` ...etc.
    EndIf
    m_ErrDetails.Locations.Add _
                   ModName & "/" & FuncName, Before:=1
    Err.Raise Err.Number
End Sub

Note that we´ve added a collection variable to the m_ErrDetails structure (which is probably a user-defined type) to save the locations in. Now we need a function to field the error and report these locations along with the error details from the event handler. Here´s one possible version:

Public Sub Catch()
    Dim sMsg As String
    Dim nThisLoc As Integer
    For nThisLoc = 1 to m_ErrDetails.Locations.Count
        sMsg = sMsg & Space(4 * (nThisLoc - 1))
        sMsg = sMsg & m_ErrDetails.Locations(nThisLoc) & vbcrlf
    Next nThisLoc

    sMsg = sMsg & Space(4 * (nThisLoc - 1))
    sMsg = sMsg & "Error (" & CStr(m_ErrDetails.Number) & ") "
    sMsg = sMsg & m_ErrDetails.Description
    MsgBox sMsg, vbExclamation, App.Title
    m_ErrDetails.Number = 0
End Sub

All the mucking about with spaces is to indent the locations so we get a nice picture of the stack at the time the error occurred. There´s also a slight complication that isn´t covered here - if the error happens in the event handler itself, there´s nothing for this Catch function to report, since the m_ErrDetails structure hasn´t been filled in. We can solve this by adding the same `ModName´ and `FuncName´ arguments to Catch (you ought to be able to work out the details yourself). Note that we also clear the error details after we´ve reported the error; in fact we also need a `Clear´ function to do this on demand for when we catch and handle an error previously thrown by Throw:

ErrorBlock:
    If Err.Number = COrderable_eNotFound Then
        ` Do something to handle the error
        ClearError
        Resume
    Else
        Throw "ThisModule", "ThisFunction"
    EndIf
End Sub

If we didn´t clear the error details here, the next unexpected error we threw would be incorrectly reported as `COrderable_eNotFound´. Incidentally, with all these functions and data we´re accumulating it looks like a good idea to gather all of our error-handling code together into its own class. A global class or BAS module works well here, which lets us qualify our various error functions (eg. GError.Throw instead of Throw). See Chapter 1 for some suggestions.

As long as we´re prepared to code an error handler in all - or at least most - of our functions we´ll get a good report when an unexpected error occurs. We can take some of the pain out of this by using a standard error handler template, but there´s no getting away from the fact that there´s a lot of extra code and a lot of typing to do (we always need to type the function name in the `Throw´ calls). The following box shows some standard templates we can use.

Standard Templates

A good start to any project is to code the following standard templates in most significant functions and all event handlers. This gives a basic error reporting infrastructure that can be expanded by adding code to the Select statement to deal with specific errors. Although the Catch function described earlier will work for classes as well as forms, we may need to consider how desirable it is to display a Message Box from a class module, and perhaps recode Catch accordingly.

for functions:

ExitBlock:
    Exit Sub
ErrorBlock:
    Select Case Err.Number
        ` Add specific error handling here as required.
        Case Else
            GError.Throw TypeName(Me), "FuncName"
    End If
End Sub

for event handlers:

ExitBlock:
    Exit Sub
ErrorBlock:
    Select Case Err.Number
        ` Add specific error handling here as required.
        Case Else
            GError.Catch TypeName(Me), "FuncName"
    End If
End Sub

Note that there´s only one line different between these two templates. It is, however, a crucial difference: coding Throw instead of Catch in the event handler is disastrous, since an unexpected error will halt the program. Note too that we´ve added another innovation, which is to use TypeName(Me) for the module name. This will work in any class or form, although for BAS files we need to type this by hand or use a module-level constant.


Method 2 (High Tech)

The method described above has the significant disadvantage that it´s enormously verbose: if we want comprehensive reporting we need to code the template in every function as well as every event handler, and we have to do a lot of typing manually. On the other hand, there´s no runtime overhead except for the On Error statements themselves, and the method will work with any version of Visual Basic we´re likely to encounter.

We´ll now look at a much more elegant way of tracking the source of a VB error that was made possible by the introduction of classes to Visual Basic. The theory is simple: define a class CTracker and put some code in its Class_Terminate event to add a string to a stack, then create an instance of CTracker in every function, initializing it with the function and module name. This is more or less what we did with the Throw function earlier, except that this time the function is called automatically when the CTracker object goes out of scope. As the error is thrown back through each function, the CTracker objects go out of scope in sequence and our location stack builds up just like before.

This scores over the first method we looked at because it needs only ONE LINE in each function, and no On Error statements. Here´s how a typical function might look:

Private Sub ThisProc()
    Dim T As New CTracker: T.Enter(TypeName(Me), "ThisProc")
    ` ...code goes here
End Sub

Clearly there´s a runtime overhead associated with this mechanism, since the objects have to be created and destroyed with every function, but its brevity is mesmerising after wading through all those error handler blocks. In practice it´s good to isolate the Tracker code in its own ActiveX DLL, and the sample implementation I´ve given below is somewhat different in that it uses a `factory´ object to create the CTracker instances. Here is the complete sample program for you to experiment with.

Error Tracking with Objects

This is a basic implementation of the error tracker. To make this work, create a project group with one ActiveX Component project and one Standard EXE. Set the EXE project as the startup project and don't forget to add a reference to the ActiveX project in Project/References.

ActiveX DLL Project

Class CStackTrace - GlobalMultiUse

Public Sub Initialize(ByVal ErrorObject As VBA.ErrObject)
    Set GStacker.ClientErrObject = ErrorObject
End Sub

Public Function Enter(ByVal ModuleName As String, _
                      ByVal ProcName As String) As CTracker
    Set Enter = New CTracker
    Enter.Module = ModuleName
    Enter.Proc = ProcName
End Function

Public Sub Report()
    GStacker.Report
    Me.Clear
End Sub

Public Sub Clear()
    GStacker.Clear
End Sub

Public Property Get GStackTrace() As CStackTrace
    Set GStackTrace = Me
End Property

Class CTracker - PublicNotCreatable

Private m_sModuleName As String
Private m_sProcName As String

Friend Property Let Module(ByVal ModName As String)
    m_sModuleName = ModName
End Property

Friend Property Let Proc(ByVal ProcName As String)
    m_sProcName = ProcName
End Property

Private Sub Class_Terminate()
    If GStacker.ClientErrObject.Number <> 0 Then
        GStacker.Leave m_sModuleName, m_sProcName
    End If
End Sub

BAS module GStacker

Private m_colStackTrace As New Collection
Private m_lErrNum As Long
Private m_sErrMsg As String
Private m_oClientErrObj As VBA.ErrObject

Public Property Set ClientErrObject( _
            ByVal ErrorObject As VBA.ErrObject)
    Set m_oClientErrObj = ErrorObject
End Property

Public Property Get ClientErrObject() As VBA.ErrObject
    Set ClientErrObject = m_oClientErrObj
End Property

Public Sub Leave(ByVal ModuleName As String, _
                 ByVal ProcName As String)
    If m_colStackTrace.Count = 0 Then
        SaveError m_oClientErrObj
    End If
    Dim colNewTrace As Collection
    Set colNewTrace = New Collection
    colNewTrace.Add ModuleName, "Module"
    colNewTrace.Add ProcName, "Proc"
    m_colStackTrace.Add colNewTrace
End Sub

Public Sub Report()
    Dim sText As String
    Dim nThisProc As Integer
    For nThisProc = m_colStackTrace.Count To 1 Step -1
        sText = sText & _
            Space(4 * (m_colStackTrace.Count - nThisProc)) & _
            m_colStackTrace(nThisProc)!Module & "/" & _
            m_colStackTrace(nThisProc)!Proc & vbCrLf
    Next nThisProc
    sText = sText & _
        Space(4 * (m_colStackTrace.Count - nThisProc)) & _
        "Error (" & CStr(m_lErrNum) & ") " & m_sErrMsg
    MsgBox sText, vbExclamation + vbOKOnly, App.Title
End Sub

Public Sub Clear()
    Set m_colStackTrace = New Collection
End Sub

Private Sub SaveError(ByVal oErr As VBA.ErrObject)
    m_lErrNum = oErr.Number
    m_sErrMsg = oErr.Description
End Sub

BAS module GStartup

Sub main()
End Sub

Standard EXE Project

Form module

Private Sub Form_Click()  ` Causes an error
    On Error GoTo ErrorBlock
    ThirdProc
    Exit Sub
ErrorBlock:
    GStackTrace.Report
End Sub

Private Sub Form_Load()  ` Causes an Error
    On Error GoTo ErrorBlock
    GStackTrace.Initialize Err
    FirstProc
    Exit Sub
ErrorBlock:
    GStackTrace.Report
End Sub

Private Sub FirstProc()
    ` NOTE: we´re doing these on two lines just to
    ` avoid wrapping!
    Dim T As CTracker
    Set T = GStackTrace.Enter(TypeName(Me), "FirstProc")
    SecondProc
End Sub

Private Sub SecondProc()
    Dim T As CTracker
    Set T = GStackTrace.Enter(TypeName(Me), "SecondProc")
    ThirdProc
End Sub

Private Sub ThirdProc()
    Dim T As CTracker
    Set T = GStackTrace.Enter(TypeName(Me), "ThirdProc")
    FourthProc
End Sub

Private Sub FourthProc()
    Dim T As CTracker
    Set T = GStackTrace.Enter(TypeName(Me), "FourthProc")
    Err.Raise 99,, "Deliberate error!"
End Sub
 

NOTE: The purpose of the Initialize method in CStackTrace isn´t immediately obvious, since the Err object is a global variable. However, it appears that the client and server components do not share the same Err object, so, for example, assigning to Err.Number in one component doesn´t affect the value in the other component. Consequently, we need to make sure it´s the client´s Err object we´re checking in CTracker´s Terminate event. To make things even more confusing, Err does behave like a shared variable when you run these components together in a project group. If in doubt, debug your components the old way, using separate instances of Visual Basic for the client and server.


So what can we do to reduce the runtime overhead of this mechanism? Well not much really, except to engineer clever ways to turn it on and off. We can 'compile it out', of course, by arranging for the active line to be in a Debug.Assert (see the the
FormSpy discussion for an example of this), but this is pretty useless because we actually want it in our production code.

A more satisfactory approach is to turn logging on and off at runtime. This means that so when tracing is off we can get rid of the function call and the object create/destroy. We still have to execute a conditional statement (which is also an additional penalty when tracing is on), but we get a more 'stealthy' trace-off mode. It needn't increase the code´s footprint appreciably - instead of

    Dim T As CTracker
    Set T = GTracer.Enter(...)

we now have

    Dim T As CTracker
    If G_Tracing Then Set T = GTracer.Enter(...)

(lines broken to avoid wrapping). Maybe we could even dispense with the GTracer factory object and get

    Dim T As New CTracker
    If GTracer.Tracing Then T.Enter(...)

Note that we´ve added a property to GTracer to keep track of whether we´re logging or not. Since the whole reason we´re doing this is to minimize processing time it´s important to make this comparison as fast as possible. Experiments show that using a `naked´ property (ie. a Public variable in the class rather than a Property Get) is between three and four times as fast, but for real speed we could throw away some of the elegance and use a global variable in a BAS file in the client program itself (around seven times as fast as a naked property).

Closing Words

The essential problem we´re trying to overcome is to hook into VB´s exception mechanism without any overhead. This is never possible, of course, because even On Error Goto <label> is an executable statement. In practice the two techniques we´ve examined here are complementary; by coding error blocks in complicated functions, for example, we can increase the likelihood of catching unexpected errors and then switch on our tracer to capture a more detailed log (assuming the error is repeatable). This doesn´t guarantee 100% coverage of unexpected errors, but it has the advantages that the object-based tracer is far less obtrusive and, in `stealth´ mode (tracing off) it´s about 40% faster than an On Error Goto.
 

Key Spinner

© 1998 - 2009 Mark Hurst. All rights reserved.   Updated March 01, 2009