1

Having the next piece of code, why do the following signatures for an event behave differently?

This one throws the exception:

    public event Action SomeEvent;

This one does not throw the exception:

    public event Func<Task> SomeEvent;

This is the code for a console application. Using the Action variant for declaring the event stops the application and shows the stack trace.

internal static class Program
{
    private static void Main()
    {
        var watcher = new Watcher();
        watcher.Context.RaiseSomeEvent();

        Console.ReadKey();
    }
}

internal class Watcher
{
    public Context Context { get; } = new Context();

    public Watcher()
    {
        Context.SomeEvent += async () =>
        {
            Console.WriteLine($"Sleeping, current thread {Thread.CurrentThread.ManagedThreadId}");
            await Task.Run(() => Thread.Sleep(1000));
            Console.WriteLine($"Slept, current thread {Thread.CurrentThread.ManagedThreadId}");
            throw new Exception();
        };
    }
}

internal class Context
{
    public event Action SomeEvent;
    //public event Func<Task> SomeEvent;

    public void RaiseSomeEvent()
    {
        SomeEvent?.Invoke();
    }
}

I had some reading such as this but I did't found anything specific to events.

chrisc
  • 105
  • 6
  • 1
    This is a result of the event lambda being `async`. Related. https://stackoverflow.com/questions/35133605/exception-from-func-not-caught-async – KDecker Feb 28 '20 at 15:16
  • 1
    Implementing correctly an async event is tricky, because you must handle the case where there are multiple subscribers to your event. You hill have to call [GetInvocationList](https://learn.microsoft.com/en-us/dotnet/api/system.delegate.getinvocationlist), then cast the delegates to the specific type of async delegate, then invoke them to get the tasks, and finally decide how to await them (sequentially or concurrently). Look here for an example: [How do I await events in C#?](https://stackoverflow.com/questions/27761852/how-do-i-await-events-in-c/27763068#27763068) – Theodor Zoulias Feb 28 '20 at 18:09

2 Answers2

5

When you use Action you are converting the lambda expression to async void, whereas the Func<Task> makes it async Task.

Async void methods have different error-handling semantics. When an exception is thrown out of an async Task or async Task method, that exception is captured and placed on the Task object. With async void methods, there is no Task object, so any exceptions thrown out of an async void method will be raised directly on the SynchronizationContext that was active when the async void method started. - https://msdn.microsoft.com/en-us/magazine/jj991977.aspx

So the exception is being thrown, irrespective of the delegate type used, however, the Func<Task> version causes the exception to be placed on the Task whereas Action results in it being thrown directly.

awaiting that Task would unwrap the exception and cause it to be thrown:

public event Func<Task> SomeEvent;

public async Task RaiseSomeEvent()
{
    await SomeEvent?.Invoke();
}
Johnathan Barclay
  • 18,599
  • 1
  • 22
  • 35
  • All are valid points. I only marked as answer the previous response because it refers to `await SomeEvent?.Invoke()` (which I overlooked, hence the confusion) and explicitly makes reference to the return on the main thread. – chrisc Feb 28 '20 at 16:38
2

The event is being thrown, but because it's being thrown in a separate thread, and nothing is being awaited in the main thread, the exception is not observed.

To fix this so the exception is observed, you would have to change the code to await like so:

using System;
using System.Threading;
using System.Threading.Tasks;

namespace Demo
{
    internal static class Program
    {
        private static async Task Main()
        {
            var watcher = new Watcher();
            await watcher.Context.RaiseSomeEvent();

            Console.ReadKey();
        }
    }

    internal class Watcher
    {
        public Context Context { get; } = new Context();

        public Watcher()
        {
            Context.SomeEvent += async () =>
            {
                Console.WriteLine($"Sleeping, current thread {Thread.CurrentThread.ManagedThreadId}");
                await Task.Run(() => Thread.Sleep(1000));
                Console.WriteLine($"Slept, current thread {Thread.CurrentThread.ManagedThreadId}");
                throw new Exception();
            };
        }
    }

    internal class Context
    {
        //public event Action SomeEvent;
        public event Func<Task> SomeEvent;

        public async Task RaiseSomeEvent()
        {
            await SomeEvent?.Invoke();
        }
    }
}
Matthew Watson
  • 104,400
  • 10
  • 158
  • 276
  • I omitted the fact that I can wait on the build-in delegate Invoke method `await SomeEvent?.Invoke()`. Don't know why I though it only has one signature and returns void. – chrisc Feb 28 '20 at 16:26