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 Task
s 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
.