Why does Dispatcher.BeginInvoke unwrap TargetInvocationException for ThreadStart but not for Action?
Asked Answered
N

1

4

Consider the following two applications:

1:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        this.Dispatcher.UnhandledException += Dispatcher_UnhandledException;
    }

    void Dispatcher_UnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
    {
        System.Diagnostics.Debug.WriteLine(e.Exception.GetType());
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        this.Dispatcher.BeginInvoke((ThreadStart)delegate
        {
            throw new AccessViolationException("test");
        }, DispatcherPriority.Input);
    }
}

2:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        this.Dispatcher.UnhandledException += Dispatcher_UnhandledException;
    }

    void Dispatcher_UnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
    {
        System.Diagnostics.Debug.WriteLine(e.Exception.GetType());
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        Dispatcher.BeginInvoke((Action)delegate
        {
            throw new AccessViolationException("test");
        }, DispatcherPriority.Input);
    }
}

Both applications are identical except for using two different delegate types, Action and ThreadStart(which have the same signature though).

Results (output window, when you invoke the event handler using a button click)

1: System.Reflection.TargetInvocationException

2: System.AccessViolationException

Why do the applications differ in behaviour?

Full stack for exception #1:

System.Reflection.TargetInvocationException: Ein Aufrufziel hat einen Ausnahmefehler verursacht. ---> System.AccessViolationException: test
   bei ExceptionTest.MainWindow.<Button_Click>b__0() in c:\Users\fschmitz\Documents\Visual Studio 11\Projects\ExceptionTest\MainWindow.xaml.cs:Zeile 40.
   --- Ende der internen Ausnahmestapelüberwachung ---
   bei System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor)
   bei System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(Object obj, Object[] parameters, Object[] arguments)
   bei System.Delegate.DynamicInvokeImpl(Object[] args)
   bei System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
   bei MS.Internal.Threading.ExceptionFilterHelper.TryCatchWhen(Object source, Delegate method, Object args, Int32 numArgs, Delegate catchHandler)
Nonbeliever answered 26/2, 2015 at 14:47 Comment(14)
so really your question is "Why does Displatcher.BeginInvoke(Action) throw and AccessViolationException?"Stripling
@DLeh: I think a more appropriate formulation is "why does Dispatcher.BeginInvoke take care to unwrap TargetInvocationException for Action but not for ThreadStart"?Trafficator
@JeroenMostert Yes, that would also be a suitable subject.Nonbeliever
What would be really suitable is the actual exception detail (call ToString on the bloody object) rather than the exception type. The TIE may have details as to why it is thrown which answers the question.Norseman
in linqpad, the following lines give me identical output: Dispatcher.CurrentDispatcher.BeginInvoke((Action)delegate { throw new AccessViolationException("test"); }, DispatcherPriority.Input); Dispatcher.CurrentDispatcher.BeginInvoke((ThreadStart)delegate { throw new AccessViolationException("test"); }, DispatcherPriority.Input);Stripling
@Will I added it to the post, but I don't think it matters here. The exception type is the important information because it shows that in one case the original exception is wrapped, and in the other case it's not.Nonbeliever
@Stripling I'm just running a normal WPF app under the VS2012 debugger, but this has implications on runtime behaviour in production environments as well. I don't use Linqpad so I can't tell what it does with that code.Nonbeliever
Interestingly if you change from Action to Action<T> (and you add the parameter) then the exception gets encapsulated like ThreadStartPneumonectomy
@Nonbeliever I was just providing some more info to help narrow down causesStripling
The TIE wraps the AVE. If you examine the code paths they are different, as in one you're handing an Action and in the other you're handling a delegate. Delegates != Actions. So, considering you're throwing an exception at the bottom of two different call stacks, you cannot reasonably assume they will result in the same exception at the top. On the delegate side, somewhere in the call stack, an object's constructor is being called, or someone is catching any exception further down the stack and rethrowing a TIE. If you honestly need to know why, trace the callstack in the codebase.Norseman
Look at the stack trace, you'll see the ExceptionFilterHelper.TryCatchWhen() method on the top. The "when" is the key. Very hard to trace inside WPF, this goes through several layers of delegates and I got lost trying to follow the chain.. Part of the code wasn't even written in C#, it uses the equivalent of VB.NET's Catch When clause.Chow
@HansPassant Thanks, I also searched the .NET framework source code without getting to the culprit. Would be interesting to know where they're hiding the relevant piece of code...Nonbeliever
@Will "Delegates != Actions" - how? Other than the type name, there is no technical difference between both delegates.Nonbeliever
That slight difference can make all the difference. Look at the answer you got :/Norseman
K
4

I think the culprit lies within InternalRealCall method of ExceptionWrapper. More specifically the following part where we have Action delegates special cased.

Action action = callback as Action;
if (action != null)
{
    action();
}
else
{
    // ... removed code ..
    obj = callback.DynamicInvoke();
}

Since Action delegates are called directly the resulting exception is the one you originally thrown. For all other delegate types, since the invocation goes through reflection the exception is wrapped in a TargetInvocationException.

In conclusion, the differences is a side-effect related to how the provided delegate is called, directly or through reflection.

Komara answered 26/2, 2015 at 15:42 Comment(2)
By following the stack trace added to the question I was able to pinpoint that code as the possible culprit.Selfish
Indeed, that makes sense, I completely missed that part of the code. My attempts tracing the invocation always ended at ExceptionFilterHelper which isn't contained in the reference sources...I should have looked at the method you posted directly :-\ Anyway, thanks!Nonbeliever

© 2022 - 2024 — McMap. All rights reserved.