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. |