Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

On Error Goto doesn't work inside EventHandler subs

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?

like image 372
Szer Avatar asked Jun 19 '15 09:06

Szer


People also ask

What does On error GoTo do?

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.

How do I use On error Resume Next in VBA?

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.


2 Answers

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:

"Non-Basic Code" between the event source and the event handler

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.

Important caveat

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.

like image 124
Mathieu Guindon Avatar answered Oct 19 '22 22:10

Mathieu Guindon


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
like image 32
Daniel Dušek Avatar answered Oct 19 '22 22:10

Daniel Dušek