Ken Puls and I were discussing the merits of custom class modules recently. Shortly after that conversation, I started rewriting a small utility app with the intention of using the Rethrow method mention by Stephen Bullen. If I’m such a class module evangelist, why am I not using a custom error object? Good question.
I decided to rewrite the PED Error Handler using a class. Below is the central error handling function with more comments than are necessary, I think.
Public Function HandleError(ByVal sModule As String, ByVal sProc As String, _
Optional ByVal sFile As String, _
Optional ByVal bEntryPoint As Boolean = False) As Boolean
Dim bReturn As Boolean
'First call, the object will be nothing so it's created
'and the number and description are saved
If gclsError Is Nothing Then
Set gclsError = New CError
gclsError.Number = Err.Number
gclsError.Message = Err.Description
End If
'Once the error number and description are captured,
'suppress all other errors
On Error Resume Next
With gclsError
'Additional properties set
.Module = sModule
.Procedure = sProc
.File = sFile
.EntryPoint = bEntryPoint
'Method to write the error out to a file
.WriteToLog
If Not .UserCanceled Then
'If it's at the entry point or in debug, display the error
If .ShouldShowMessage Then
Application.ScreenUpdating = True
MsgBox .Message, vbCritical, gsAPPTITLE
Set gclsError = Nothing
Else
'Rethrow the error in the calling procedure
On Error GoTo 0
Err.Raise .Number, .FullSource, .Message
End If
bReturn = .DebugMode
Else
'End silently and kill the object
bReturn = False
Set gclsError = Nothing
End If
End With
HandleError = bReturn
End Function
This isn’t an exact replacement for the one in the book. It only uses the Rethrow method, so it won’t be a good solution if you need to clean up after an error. My goal was not to duplicate it exactly, but rather to kill some time during one of the less relevant MVP Summit sessions. Here are a couple of highlights:
I wrote a write-once property for the Message property. Later, I changed the main function to only write the Message property when a new CError object is created so it’s redundant.
Public Property Let Message(ByVal sMessage As String)
If Len(Me.Message) = 0 Then msMessage = sMessage
End Property
Writing to the log file uses some other custom properties that are basically string builders.
Public Sub WriteToLog()
Dim lFile As Long
On Error Resume Next
lFile = FreeFile
Open Me.LogFile For Append As lFile
Print #lFile, Format$(Now(), "dd mmm yy hh:mm:ss"); Me.LogEntry
If Me.EntryPoint Then
Print #lFile,
End If
Close lFile
End Sub
I modified the standard Let Number property to use a default “User Cancel” message.
Public Property Let Number(ByVal lNumber As Long)
mlNumber = lNumber
If lNumber = ErrorType.UserCancel Then
Me.Message = msUSERCANCEL
End If
End Property
One of the things I like about using class modules is turning Boolean logic into easy-to-understand English. I could have coded
If .DebugMode Or .EntryPoint Then
but I much prefer to see
If .ShouldShowMessage Then
and to put that Boolean logic in the property
Public Property Get ShouldShowMessage() As Boolean
ShouldShowMessage = Me.DebugMode Or Me.EntryPoint
End Property
I get the benefit of using and reusing ShouldShowMessage wherever I want and if the logic changes, I change it only in one place. I only use it once and probably won’t use it anywhere else, but beyond that I just like that the intent is embedded in the code so the reader doesn’t have to try to figure it out unless they want to.
And here’s some fake code to see if it works.
Sub Main()
Dim lResp As Long
Const sSOURCE As String = "Main()"
On Error GoTo ErrorHandler
lResp = MsgBox("Cancel?", vbYesNo, gsAPPTITLE)
If lResp = vbYes Then
Err.Raise ErrorType.UserCancel, sSOURCE
Else
Sub_Procedure
End If
Exit Sub
ErrorHandler:
If HandleError(msMODULE, sSOURCE, , True) Then
Stop
Resume
End If
End Sub
Sub Sub_Procedure()
Dim i As Long
Const sSOURCE As String = "Sub_Procedure()"
On Error GoTo ErrorHandler
i = Sub_Function(1) 'no error here
i = Sub_Function(0) 'this will create a divide by zero
Exit Sub
ErrorHandler:
If HandleError(msMODULE, sSOURCE) Then
Stop
Resume
End If
End Sub
Function Sub_Function(lDenom As Long) As Long
Dim i As Long
Const sSOURCE As String = "Sub_Function()"
On Error GoTo ErrorHandler
i = 1 / lDenom 'When zero is passed in, an error is raised
Exit Function
ErrorHandler:
If HandleError(msMODULE, sSOURCE) Then
Stop
Resume
End If
End Function
Thanks to Bob Phillips for telling me to use an Enum instead of a constant: ErrorType.UserCancel vs. glUSERCANCEL.
You can download ErrorClass.zip
Posting code? Use <pre> tags for VBA and <code> tags for inline.