1

I made an event system which works nicely and allows me to register an event handler by passing an event type and a handler, which is an action, such as

EventManager.RegisterEventHandler<Events.NewCompleteSample>(HandleCompleteSample);

calling

public static void RegisterEventHandler<T>(Action<T> handlerAction)

When the event is later raised, I call the actions which were registred.

My problem is that I randomly have silent crashes, and I now realize that this might be because the actions I call may (frequently are) async methods, and too late I am learning that exceptions in non awaited async methods are not caught anywhere.

The solution seems to be to await the handler like

await handler.Invoke(arguments)

but since I know of no way to specify that the Actions are async methods, this will not work.

Is there a way to define an Action as async, so you can await it when later calling it, or might there be a smarter solution to this issue?

JoeTaicoon
  • 1,383
  • 1
  • 12
  • 28
  • 8
    Yes, async action is `Func` – Fabio Feb 02 '22 at 19:13
  • "Async function" is an unforunately common term for what's really an "awaitable function". If you think of it *that* way, it makes better sense. The `async` keyword has to do with the async state machine that the compiler creates for you, but you don't need that for something to be awaitable. – madreflection Feb 02 '22 at 19:18
  • Thanks @Fabio, that was quick and seems to work in test. Will experiment. – JoeTaicoon Feb 02 '22 at 19:20
  • I am not entirely sure I follow, @madreflection. If something is not marked as async and sets the statemachine, then how can you await it? – JoeTaicoon Feb 02 '22 at 19:20
  • 1
    It's awaitable because it follows a pattern. `Task` and `Task`, along with `ValueTask` and `ValueTask` all follow the pattern, making it awaitable so you can use the `await` keyword. Not all awaitable functions have the `async` keyword because they don't need the compiler to rewrite the method into a state machine. Sometimes, they just `return Task.Completed;`. That doesn't require a state machine, but it's awaitable nonetheless. – madreflection Feb 02 '22 at 19:23
  • So with something like `Func func = () => Task.Completed;` , `func` is an awaitable delegate because it returns `Task`. That's functionally identical to `Func func = async () => { };`, except that the latter delegate gets converted into a state machine for no additional benefit. – madreflection Feb 02 '22 at 19:26
  • 1
    Possibly related question: [How do I await events in C#?](https://stackoverflow.com/questions/27761852/how-do-i-await-events-in-c) – Theodor Zoulias Feb 02 '22 at 19:27
  • Thanks for elaborating. That makes sense. – JoeTaicoon Feb 02 '22 at 19:29
  • @madreflection *"awaitable function"* is not a great term. It implies that you can await a function like this: `await MyFunction`, which doesn't compile. What is awaitable is not the function itself, it's the result of the function. The correct term is *"asynchronous function"* IMHO. – Theodor Zoulias Feb 02 '22 at 19:31
  • @TheodorZoulias: You make a good point, but I still think it's an improvement over "async function" because methods that aren't marked `async` can be asynchronous in nature and therefore awaitable. Yes, "awaitable object" is more accurate, but I think that's overly pedantic for this purpose. OP's misunderstanding was not related to method group references vs. method calls. – madreflection Feb 02 '22 at 19:34
  • Furthermore, Visual Studio puts "(awaitable)" in the parameter help tooltip, so the term maps well, even if, again, it's really that the returned object is awaitable (and the annotation's position could be referring to either the return value or the method but it doesn't draw a distinction and I don't think it needs to). – madreflection Feb 02 '22 at 19:36
  • (sorry, on earlier comments, it should've been `Task.CompletedTask`... I always forget that) – madreflection Feb 02 '22 at 19:40
  • Unfortunately, Visual Studio doesn't annotate the parameter tooltip for VB, where the return type comes at the end of the declaration, so there's no A/B comparison to see which is actually being annotated in the tooltip. If it had been annotated at the end in VB. If it were the returned object, I would expect it to be at the end of the declaration, vs. C# which has it at the beginning. For whatever that's worth. – madreflection Feb 02 '22 at 19:44
  • @madreflection asynchronous is a method that returns an awaitable type. Whether it's implemented with or without `async` is not important. Some would say that in order to be asynchronous, the method should also have an asynchronous implementation. I've gone down this [rabbit hole](https://stackoverflow.com/a/61837980/11178549) in the past, without reaching a definite conclusion. – Theodor Zoulias Feb 02 '22 at 19:45
  • *"Whether it's implemented with or without async is not important."* - That's exactly the point I'm making. What matters is that it's awaitable. And that's why I suggested calling it that, even if it's really the return value that's awaitable. – madreflection Feb 02 '22 at 19:46
  • I appreciate that you went down that rabbit hole. I just wish the documentation hadn't settled on that term because I think it leads to confusion. – madreflection Feb 02 '22 at 19:49
  • I think the question you answered further highlights the misunderstanding caused by that terminology. I don't disagree with the `async` keyword, just the term used to describe the union of methods that are marked `async` and methods that are *not* marked `async` but are still awaitable. – madreflection Feb 02 '22 at 19:54
  • @madreflection I should point out that `await` means literally asynchronous wait. And awaitable means something that can be waited asynchronously. So going from "asynchronous method" to "awaitable method" just adds the "wait" term in the mix. The "asynchronous" didn't go anywhere, it's still here. – Theodor Zoulias Feb 02 '22 at 20:07
  • I edited the question to show my redesigned attempt using the feedback from everyone here so far. Did I miss some nasty pitfall here, or is it worth moving forward with? – JoeTaicoon Feb 03 '22 at 07:47

2 Answers2

4

My problem is that I randomly have silent crashes, and I now realize that this might be because the actions I call may (frequently are) async methods, and too late I am learning that exceptions in non awaited async methods are not caught anywhere.

Sort of. Exceptions thrown from normal asynchronous methods (i.e., ones that return Task or Task<T>) are just ignored if the task is never observed. Exceptions thrown from async void methods do crash the process; this is a deliberate design decision.

Is there a way to define an Action as async,

Sure; just map delegates to function signatures, make them asynchronous, and then map back. That will give you the asynchronous delegate types (as described on my blog).

In this case, Action maps to a method like void Handle(); the asynchronous version of which is Task HandleAsync(), which maps to a delegate of type Func<Task>.

so you can await it when later calling it,

Welllll, sort of. Sure, await handler.Invoke(); will compile, but events (and delegates) can hold any number of handlers, and Invoke just returns the last result, so the await awaits the Task returned from the last handler. If there are multiple handlers, all other Tasks are discarded (and their exceptions silently swallowed).

or might there be a smarter solution to this issue?

There's a few solutions for asynchronous events (link to my blog).

First, I'd consider adjusting the design. Events are a good match for the Observer design pattern, but the need to await handlers usually means events are being misused to implement a different design pattern, usually the Command or Strategy design patterns. Consider replacing the event completely with something different, like an interface.

If that isn't feasible, then I'd consider using a different event representation by investigating Reactive Observables. Observables are really what events should be. But observables can get pretty complex.

Another approach is to use deferrals. Essentially, you keep the async void method but the handlers need to "enlist" in asynchronous handling by acquiring a deferral in a using. The invoking code then waits for all deferrals to be released.

The final approach is to use Func<Task>, get all the tasks via GetInvocationList, and await them one at a time or with Task.WhenAll.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Thank you for the long answer. I am not easily able to change the design fundamentally right now. As it is the event raising and handling is 100% decoupled by doing this through an event manager, and that makes a lot of things super simple. The issue of awaiting the handler Func in unclear. If that handler Func is async and awaits another async method which may await yet another, will awaiting the top Func not be sure to catch exceptions down the entire chain? – JoeTaicoon Feb 03 '22 at 05:26
  • @JoeTaicoon: Yes, it will, as long as there are no `async void` methods in the chain. – Stephen Cleary Feb 03 '22 at 06:44
-1

You can use a Func<object, Task> to create an "awaitable action" that receives a parameter of type object and does not have a return type. https://learn.microsoft.com/en-us/dotnet/api/system.func-2?f1url=%3FappId%3DDev16IDEF1%26l%3DEN-US%26k%3Dk(System.Func%602);k(SolutionItemsProject);k(DevLang-csharp)%26rd%3Dtrue&view=net-6.0

    private async Task ExecuteAsync(Func<object, Task> action, object args)
    {
       await action.Invoke(args);
    }
Murilo Maciel Curti
  • 2,677
  • 1
  • 21
  • 26
  • 1
    *"and does not have a return type"* -- The `Func` does have a return type. The return type is a `Task`. You probably mean that this `Task` is a non-generic task. It doesn't have a [`Result`](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task-1.result) property. – Theodor Zoulias Feb 03 '22 at 04:52
  • Good point but is better to keep the the original text because it uses the same kind of language as the question – Murilo Maciel Curti Feb 03 '22 at 14:54