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

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.
 

Key Spinner

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