For example, our error object has extra methods named Push and Pop. These, mostly for historical reasons, are really useful methods because it's not clear in Visual Basic when Err.Clear is actually applied to the Err object-that is, when the outstanding error, which you've been called to handle, is automatically cleared. This can easily result in the reporting of error 0. Watch out for this because you'll see it a lot!
Usually, an error is mistakenly cleared in this way when someone is handling an error and from within the error handler he or she calls some other routine that causes Visual Basic to execute an Err.Clear. All sorts of things can make Visual Basic execute an Err.Clear. The result in this case is that the error is lost! These kinds of mistakes are really hard to find. They're also really easy to put in-lines of code that cause this to happen, that is!
The Help file under Err Object used to include this Caution about losing the error context.
If you set up an error handler using On Error GoTo and that handler calls another procedure, the properties of the Err object may be reset to zero and zero-length strings. To retain values for later use, assign the values of Err properties to variables before calling another procedure, or before executing Resume, On Error, Exit Sub, Exit Function, or Exit Property statements.
Of course, if you do reset Err.Number (perhaps by using On Error GoTo in the called routine), when you return to the calling routine the error will be lost. The answer, of course, is to preserve, or push, the error context onto some kind of error stack. We do this with Err.Push. It's the first line of code in the error handler-always. (By the way, Visual Basic won't do an Err.Clear on the call to Err.Push but only on its return-guaranteed.) Here's an example of how this push and pop method of error handling looks in practice:
Private Sub Command1_Click() On Error GoTo error_handler: VBA.Err.Raise 42 Exit Sub error_handler: Err.Push Call SomeFunc Err.Pop MsgBox Err.Description Resume Next End Sub
Here we're raising an error (42, as it happens) and handling it in our error handler just below. The message box reports the error correctly as being an Application Defined Error. If we were to comment out the Err.Push and Err.Pop routines and rerun the code, the error information would be lost and the message box would be empty (as Err.Number and Err.Description have been reset to some suitable "nothing"), assuming the call to SomeFunc completes successfully. In other words, when we come to show the message box, there's no outstanding error to report! (The call to Err.Push is the first statement in the error handler. This is easy to check for during a code review.)
If we assume that Visual Basic itself raises exceptions by calling Err.Raise and that Err.Raise simply sets other properties of Err, such as Err.Number, our own Err.Number obviously won't be called to set VBA.Err properties (as it would if we simply had a line of code that read, say, Err.Number = 42). This is a pity because if it did call our Err.Number, we could detect (what with our Err.Number being called first before any other routines) that an error was being raised and automatically look after preserving the error context; that is, we could do an Err.Push automatically without having it appear in each error handler.
All sound good to you? Here's a sample implementation of a new Err object that contains Pop and Push methods:
In a class called ErrObject
Private e() As ErrObjectState Private Type ErrObjectState Description As String HelpContext As Long HelpFile As String Number As Long End Type Public Property Get Description() As String Description = VBA.Err.Description End Property Public Property Let Description(ByVal s As String) VBA.Err.Description = s End Property Public Property Get HelpContext() As Long HelpContext = VBA.Err.HelpContext End Property Public Property Let HelpContext(ByVal l As Long) VBA.Err.HelpContext = l End Property Public Property Get HelpFile() As String HelpFile = VBA.Err.HelpFile End Property Public Property Let HelpFile(ByVal s As String) VBA.Err.HelpFile = s End Property Public Property Get Number() As Long Number = VBA.Err.Number End Property Public Property Let Number(ByVal l As Long) VBA.Err.Number = l End Property Public Property Get Source() As String Source = VBA.Err.Source End Property Public Property Let Source(ByVal s As String) VBA.Err.Source = s End Property Public Sub Clear() VBA.Err.Clear Description = VBA.Err.Description HelpContext = VBA.Err.HelpContext HelpFile = VBA.Err.HelpFile Number = VBA.Err.Number End Sub Public Sub Push() ReDim Preserve e(UBound(e) + 1) As ErrObjectState With e(UBound(e)) .Description = Description .HelpContext = HelpContext .HelpFile = HelpFile .Number = Number End With End Sub Public Sub Pop() With e(UBound(e)) Description = .Description HelpContext = .HelpContext HelpFile = .HelpFile Number = .Number End With If UBound(e) Then ReDim e(UBound(e) - 1) As ErrObjectState Else VBA.Err.Raise Number:=28 ' Out of stack space - underflow End If End Sub Private Sub Class_Initialize() ReDim e(0) As ErrObjectState End Sub Private Sub Class_Terminate() Erase e() End Sub
In Sub Main
Set Err = New ErrObject
In Global Module
Public Err As ErrObject
As you can see, our new Err object maintains a stack of a user-defined type (UDT) called ErrObjectState. An instance of this type basically holds information from the last error. In Sub Main we create our only ErrObject-note that it's called Err. This means that calls to methods like Err.Number will be directed to our object. In other words, Err refers to our instance of ErrObject and not the global instance VBA.Err. This means, of course, that we have to provide stubs for all the methods that are normally part of the global Err object: Number, Description, Source, and so on.
Note that we've left LastDLLError off the list. This is because when we pop the stack we'd need to write a value back into VBA.Err.LastDLLError and, unfortunately, this is a read-only property!
Another object we replace is the Debug object. We do this because we sometimes want to see what debug messages might be emitting from a built executable.
As you know, "normal" Debug.Print calls are thrown away by Visual Basic when your application is running as an executable; "special" Debug.Print calls, however, can be captured even when the application is running as an executable. Replacing this object is a little trickier than replacing the Err object because the Debug object name cannot be overloaded; that is,you have to call your new object something like Debugger. This new object can be designed to write to Visual Basic's Immediate window so that it becomes a complete replacement for the Debug object. Chapter 6 shows how you can write your own Assert method so that you can also replace the Debug object's Assert method.