Visual Basic

Tip 11: Replace useful intrinsic objects with your own.

Our main ROOS contains a set of alternative standard object classes, TMSErr and TMSApp, for example. These are instantiated as Err and App at application start-up as part of our application template initialization. (All our Visual Basic applications are built on this template.) By creating objects like this, we can add methods, properties, and so on to what looks like one of Visual Basic's own objects.

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
      Call SomeFunc
      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()
      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
          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.