If you still use VB4 or VB5, there's an error-handling issue that you need to be aware of when coding Terminate events. We'll have a look at this in detail here, and then
we'll have a look at a peculiar instability in the Initialize event. Consider the following skeleton code, which deliberately creates an error condition in a nested subroutine: Sub FirstSub()
On Error Goto ErrorBlock SecondSub Exit Sub ErrorBlock: ' Deal with the error here End Sub
Sub SecondSub()
Dim MyObject As CMyClass Set MyObject = New CMyClass ... Err.Raise SOMETHING,,"Deliberate error" ...
End Sub When SecondSub throws the error, the next piece of VB code to be executed is the CMyClass.Terminate event handler (not shown here). This code is executed before
control passes to the error handler block in FirstSub, and it happens because the object referenced by MyObject is destroyed when that variable goes out of scope. Crucially, anything we do in the Terminate event that affects the error context will mess things up when control eventually gets to the error handler in FirstSub. Bearing in mind that that Terminate event
must
contain an On Error statement if the program is to be robust, this will wipe out the context and the error number will be 0 when control gets back to FirstSub. Incidentally, if we're using an error handling strategy that involves re-raising errors from error handlers (see
Chapter 8 for such a strategy), this will generate an Illegal Function Call (error 5), since 0 isn't a valid error number. Now let's look at
what happens if the Terminate event itself throws an error. Just to recap, we're talking about a specific chain of events that leads to a Terminate event firing as VB is trying to unwind the stack to an active error
handler in a calling function, and then to a subsequent error being generated inside that Terminate event. This means that in principle there are two
unhandled errors outstanding at once. As we'll see, VB doesn't deal at all well with this situation. We've already noted that the event handler must contain an On Error statement, and we'll assume this is an On Error GoTo that jumps to an error block in the Terminate event handler. One of two things now happens:
- If FirstSub is at the bottom of a chain of function calls that originated in an event handler, control passes back to that event handler immediately. No other error handlers are invoked and the original error
vanishes without trace.
- If FirstSub is at the bottom of a chain of function calls that originated in Sub Main, VB displays a message box reporting 'Run-time Error 0' and ends the program,
regardless of whether Sub Main has an error handler.
So what's going on? We'll probably never know, but it's something to do with the Terminate event handler pre-empting our code. This event handler is logically a new stream of execution [I'm trying hard not to say
'thread' here, as this problem pre-dates COM threading in Visual Basic], invoked asynchronously by Visual Basic, and the problem is that it messes things up for the pre-empted code because it destroys the error context.
Note that it doesn't wipe out the error condition, since control does pass back to FirstSub's error handler and not the line following the call to SecondSub. The flow of control is exactly what we'd expect from
VB's way of propagating errors, it's just that the intervening Terminate event destroys the evidence. This is faulty behaviour, and we can speculate that it's because the introduction of asynchronous Terminate events in
VB4 has revealed a fundamental inadequacy in VB's error handling mechanism. VB is designed to propagate a single error condition back up through the calling hierarchy, but since we can provoke what is effectively a
multi-threading situation with two distinct segments of code executing, the mechanism falls apart. In fact this isn't strictly a problem with Terminate events at all, and the following code will also provoke it if
Text1_Change has an On Error statement in it: ... ErrorBlock: Text1.Text = "Something" Err.Raise Err.Number,, _
Err.Description ' Illegal function call End Sub The fact that this code wipes out the error context isn't controversial, of course,
it's a rough edge that's familiar enough to VB programmers and it's why sooner or later everyone gets around to implementing Err.Push and Err.Pop. The difference is that we can choose not
to fiddle with a control here and so avoid the problem, whereas Terminate events are beyond our control if they're used as intended (but see below for how to provoke them). It seems likely that this was an important distinction that was overlooked when VB4 sprouted Initialize and Terminate events. The bottom line is,
if we throw a valid error number from a function we can arrive in another error handler with Error 0. This is useless at best, but perhaps more worrying is the way the rest of the code behaves in dealing with
the original error when the Terminate event raises and handles an error of its own. The class should behave like a black box, and having an error handler in the Terminate event should make it watertight, but the
behaviour of our original code is modified depending on whether or not the Terminate handled an error of its own. The only explanation I can suggest for this weird behaviour is that it's a kludge which acknowledges the
inadequacy of the single-threaded error handling mechanism. Returning to our original example, what can we do to avoid the problem? We can, of course, refrain from putting On Error statements in our Terminate events,
but this means we must keep them very simple so as to avoid the possibility of ever generating runtime errors. A better way is to destroy object references explicitly, which puts the invocation of Terminate events under
our control. Here's how SecondSub's error handler should look if we take this approach: ... ErrorBlock: Dim SavedErr As Long SavedErr = Err.Number
Set MyObject = Nothing ' Terminate runs now Err.Number = SavedErr Exit Sub End Sub This is safe, but apart from all that fiddling with saving the error
details it's a nuisance because we must have an error handler in any routine that defines an instance of a class. The Instability of Initialize
The propagation of unhandled errors from Initialize event handlers in forms and classes is not the same as it is for other events. Conventional wisdom says an untrapped error in an event handler stops the program,
but Initialize events are different: an error thrown by an Initialize is raised on the line in the client that creates the object. This means that the following code stops with error 30000 at the arrowed line:
Class1 Private Sub Class_Initialize() Error 30000 End Sub Module1 Private x As Class1 Sub main() Set x = New Class1 ' <--- Error 30000 here
End Sub However, this behaviour doesn't seem to be consistent because if Class1 is in the same project as Sub Main, the behaviour differs according to whether or not we have an error handler in the Initialize event –
even if that error handler just throws the error again. For example, this doesn't work: Class1 Private Sub Class_Initialize() On Error Goto ErrorBlock Error 30000
Exit Sub ErrorBlock: Err.Raise Err.Number End Sub Module1 Private x As Class1 Sub main()
Set x = New Class1 ' <--- Error 440 here! End Sub Error 440 is 'Automation error'. This difference in behaviour doesn't occur if Class1 is in a DLL. All of this is also true if Class1 is
a form (Form2, say), although obviously we can never export Form2 from a DLL. Now switching specifically to forms, some strange things happen when we make Form2 the Startup object of our project (ie. we get rid of Sub
Main). Just to remind you, this is what Form2 contains: Private Sub Form_Initialize() Error 30000 End Sub What we'd expect to
happen is for VB to terminate the program and display the message 'Run-time error 30000 / Application-defined or object-defined error'. What it actually does is this: A bit weird, but there's worse to come. Let's add back the error handler: Private Sub Form2_Initialize() On Error Goto ErrorBlock
Error 30000 Exit Sub ErrorBlock: Err.Raise Err.Number
End Sub Now things get really weird, because VB does this: This happens on both VB6 (Windows 98) and VB5/SP3 (Windows 95), and similar results are reported for NT4 with both VB5/SP3 and VB6/SP2. Compiling to an EXE restores the original empty message
box on all three operating systems, which suggests that we´re seeing the same problem manifest itself in two different ways according to the context. What worries me are the possible causes of this strange behaviour:
- Is it a bug in the startup code of the VB runtime system?
- Is it a bug in forms and/or classes that usually goes unnoticed because it doesn't cause an exception?
- Is it a bug in VB error handling that means we should never throw errors from an active error handler in a Class_Initialize or Form_Initialize event?
If the first explanation is the right one then it doesn't seem to be much of a problem, since we can just avoid this rather peculiar situation. The other two explanations are more worrying,
particularly the second. Given that leaking errors from Initialize events is a documented technique, the last explanation seems the most likely and is relatively easy to comply with. |