Lets assume this code:
Module1:
Sub main()
Dim cl As New Class2
On Error GoTo errorhandler1
cl.DoWork
On Error GoTo 0
Exit Sub
errorhandler1:
MsgBox (Err.Description)
End Sub
Class1:
Event MyEvent()
Public Sub DoWork()
RaiseEvent MyEvent
End Sub
Class2:
Private WithEvents cl As Class1
Private Sub cl_MyEvent()
Call Err.Raise(123, , "ErrorInClass")
End Sub
Private Sub Class_Initialize()
Set cl = New Class1
End Sub
Public Sub DoWork()
cl.DoWork
End Sub
I expect errorhandler1 to launch and MsgBox with err.Description to be shown. But it throws me runtime error instead.
What I have to do to handle errors within EventHandlers routines?
On Error GoTo 0 disables error handling in the current procedure. It doesn't specify line 0 as the start of the error-handling code, even if the procedure contains a line numbered 0. Without an On Error GoTo 0 statement, an error handler is automatically disabled when a procedure is exited.
On Error Resume Next tells VBA to continue executing statements immediately after the statement that generated the error. On Error Resume Next allows your code to continue running even if an error occurs. Resume Next does not fix an error, it just ignores it. This can be good and bad.
This has just bitten me - you see in this simple C# code:
try
{
SomeEvent?.Invoke(this, EventArgs.Empty);
}
catch
{
// break here
}
If any handler of SomeEvent
throws an exception, AFAIK a breakpoint in that catch
block would be hit - and I was expecting VBA to do the same... and it doesn't.
By breaking in an event handler, and then inspecting the call stack, you can see that there's something slipping between the event source with the RaiseEvent
call, and the event handler procedure:
I presume that [<Non-Basic code>]
here would be the VBA runtime itself, dispatching the event to whatever object is listening for events on that particular event source instance: and this "man-in-the-middle" is quite likely why run-time errors aren't bubbling back up: the runtime is likely protecting itself and throwing an error here, regardless of whether the parent stack frame has an On Error
statement.
You can see a hint of my work-around in that screenshot - add a new class module, call it ErrorInfo
, and give it some useful members:
Option Explicit
Private Type TErrorInfo
Number As Long
Description As String
Source As String
End Type
Private this As TErrorInfo
Public Property Get Number() As Long
Number = this.Number
End Property
Public Property Get Description() As String
Description = this.Description
End Property
Public Property Get Source() As String
Source = this.Source
End Property
Public Property Get HasError() As Boolean
HasError = this.Number <> 0
End Property
Public Property Get Self() As ErrorInfo
Set Self = Me
End Property
Public Sub SetErrInfo(ByVal e As ErrObject)
With e
this.Number = .Number
this.Description = .Description
this.Source = .Source
End With
End Sub
Now whenever you define an event, add a parameter to it:
Public Event Something(ByVal e As ErrorInfo)
When you raise that event, supply the instance, handle errors, inspect your ErrorInfo
object, invoke Err.Raise
accordingly, and you can handle that error normally, in the event-invoking scope you want to handle event-handler errors in:
Public Sub DoSomething()
On Error GoTo CleanFail
With New ErrorInfo
RaiseEvent Something(.Self)
If .HasError Then Err.Raise .Number, .Source, .Description
End With
Exit Sub
CleanFail:
MsgBox Err.Description, vbExclamation
End sub
The event handler code simply needs to handle its errors (any run-time error in a handler is basically unhandled otherwise), and set the error state in the ErrInfo
parameter:
Private Sub foo_Something(ByVal e As ErrorInfo)
On Error GoTo CleanFail
Err.Raise 5
Exit Sub
CleanFail:
e.SetErrInfo Err
End Sub
And bingo, now you can cleanly handle errors raised in an event handler, at the event source, without involving global variables or losing the actual error information (in my case, an error thrown in some 3rd-party API) to some useless (but arguably "good-enough" in most cases) "oops, didn't work" message.
As is the case with Cancel
events, if an event has multiple handlers, then which state goes back to the event invocation site is, well, undefined - if only one handler throws an error, and the non-throwing handlers don't tamper with the ErrorInfo
parameter, then in theory the invocation site gets the one error. The "fun" begins when two or more handlers throw an error.
In that case, the handlers need to verify what the state of the ErrorInfo
is before they modify it.
Or, another solution could be to make the ErrorInfo
class encapsulate an array of error information, and perhaps add indexers to the Property Get
members - or whatever other mechanism you could think about, to "aggregate errors". Heck, you could even encapsulate a collection of ErrorInfo
instances in an AggregateErrorInfo
collection class, and make your "multiple-listeners event" use that in its signature instead.
Most of the time you only need a single handler though, so this wouldn't be a concern.
As we can read here:
If you use the Raise method of the Err object to raise an error, you can force Visual Basic to search backward through the calls list for an enabled error handler.
But in this case there is no enabled error handler.
Maybe you could inform the client of class2 that the work failed. Here because the client of class2 is a standard module you can't use events from class2, so maybe just a simple read-only property might help here?
Module:
Sub main()
cl.DoWork
If Not cl.IsWorkOk Then MsgBox "Work failed..."
On Error GoTo 0
Exit Sub
errorhandler1:
MsgBox (Err.Description)
End Sub
Class2:
Private m_isWorkOk As Boolean
Private Sub cl_MyEvent()
On Error GoTo ErrMyEvent
Call Err.Raise(123, , "ErrorInClass")
m_isWorkOk = True
Exit Sub
ErrMyEvent:
m_isWorkOk = False
End Sub
Public Property Get IsWorkOk() As Boolean
IsWorkOk = m_isWorkOk
End Property
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With