--------------------------------------------------------------------------------
Tip 22: Always handle errors in controls and components (that you build).
Errors in controls
When a control raises an unhandled error (by the control), the error is reported and the control becomes disabled—it actually appears hatched—or the application terminates. (See Figure 1-7 and Figure 1-8.)
Figure 1-7 Containing form before the error
Figure 1-8 Containing form after the error
It's important to know that errors in a UserControl can be propagated to two different levels. If the errors are caused wholly by the control, they will be handled by the control only. If the errors are instigated via a call to an external interface on the control, from the containing application, they will be handled by the container. Another way to state this is to say that whatever is at the top of the call stack will handle unhandled errors. If you call into a control, say from a menu selection in the container, the first entry in your call stack will be the container's code. That's where the mnuWhatever_Click occurred. If the control raises an error now, the call stack is searched for a handler, all the way to the top. In this case, any unhandled control error has to be handled in the container, and if you don't handle it there, you're dead when the container stops and, ergo, so does the control. However, if the control has its own UI or maybe a button, your top-level event could be a Whatever_Click generated on the control itself. The top of your call stack is now your control code and any unhandled errors cause only the control to die. The container survives, albeit with a weird-looking control on it. (See Figure 1-8.)
This means that you must fragment your error handling across containers and controls, not an optimal option. Or you need some way of raising the error on the container even if the container's code isn't on the stack at the moment the error occurs. A sort of Container.Err.Raise thing is required.
In each of our container applications (those applications that contain UserControls), we have a class called ControlErrors (usually one instance only). This class has a bundle of miscellaneous code in it that I won't cover here, and a method that looks something like this:
Public Sub Raise(ParamArray v() As Variant)
On Error GoTo ControlErrorHandler:
' Basically turns a notification into an error -
' one easy way to populate the Err object.
Err.Raise v(0), v(1), v(2), v(3), v(4)
Exit Sub
ControlErrorHandler:
MsgBox "An error " & Err.Number & " occurred in " & Err.Source & _
" UserControl. The error is described as " & Err.Description
End Sub
In each container application we declare a new instance of ControlErrors, and for each of our UserControls we do what's shown below.
If True = UserControl1.UsesControlErrors Then
Set UserControl1.ErrObject = o
End If
UsesControlErrors returns True if the UserControl has been written to "know" about a ControlErrors object.
In each control—to complete the picture—we have something like this (UsesControlErrors is not shown):
Private ContainerControlErrors As Object
Private Sub SomeUIWidget_Click()
On Error GoTo ErrorHandler:
Err.Raise ErrorValue
Exit Sub
ErrorHandler:
' Handle top-level event error.
' Report error higher up?
If Not ContainerControlErrors Is Nothing Then
ContainerControlErrors.Raise ErrorValue
End If
End Sub
Public Property Set ErrObject(ByVal o As Object)
Set ContainerControlErrors = o
End Property
We know from this context that SomeUIWidget_Click is a top-level event handler (so we must handle errors here), and we can make a choice as to whether we handle the error locally or pass it on up the call chain. Of course, we can't issue a Resume Next from the container once we've handled the (reporting of the) error—that's normal Visual Basic. But we do at least have a mechanism whereby we can report errors to container code, perhaps signalling that we (the control) are about to perform a Resume Next or whatever.
Errors in OLE servers
Raising errors in a Visual Basic OLE Automation server is much the same as for a stand-alone application. However, some consideration must be given to the fact that your server may not be running in an environment in which errors will be visible to the user. For example, it may be running as a service on a remote machine. In these cases, consider these two points:
Don't display any error messages. If the component is running on a remote machine, or as a service with no user logged on, the user will not see the error message. This will cause the client application to lock up because the error cannot be acknowledged.
Trap every error in every procedure. If Visual Basic's default error handler were executed in a remote server, and assuming you could acknowledge the resulting message box, the result would be the death of your object. This would cause an Automation error to be generated in the client on the line where the object's method or property was invoked. Because the object has now died, you will have a reference in your client to a nonexistent object.
To handle errors in server components, first trap and log the error at the source. In each procedure, ensure that you have an Err.Raise to guarantee that the error is passed back up the call stack. When the error is raised within the top-level procedure, the error will propagate to the client. This will leave your object in a tidy state; indeed, you may continue to use the same object.
If you are raising a user-defined error within your component you should add the constant vbObjectError (&H80040000&). Using vbObjectError causes the error to be reported as an Automation error. To extract the user-defined error number, subtract vbObjectError from Err.Number. Do not use vbObjectError with Visual Basic-defined errors; otherwise, an "Invalid procedure call" error will be generated.