I understand that an async Task
's Exceptions can be caught by:
try { await task; }
catch { }
while an async void
's cannot because it cannot be awaited.
But why is it that when the async Task is not awaited (just like the async void one) the Exception
is swallowed, while the void's one crashes the application?
Caller: ex();
Called:
async void ex() { throw new Exception(); }
async Task ex() { throw new Exception(); }
Please read important note at bottom.
The
async void
method will crash the application because there is noTask
object for the C# compiler to push the exception into. On a functional level, theasync
keyword on aTask
-returning method is just heavy syntax sugar that tells the compiler to rewrite your method in terms of aTask
object using the various methods available on the object as well as utilities such asTask.FromResult
,Task.FromException
, andTask.FromCancelled
, or sometimesTask.Run
, or equivalents from the compiler's point of view. This means that code like:gets turned into approximately:
and so when you call
Task
-returningasync
methods thatthrow
, the program doesn't crash because no exception is actually being thrown; instead, aTask
object is created in an "excepted" state and is returned to the caller. As mentioned previously, anasync void
-decorated method doesn't have aTask
object to return, and so the compiler does not attempt to rewrite the method in terms of aTask
object, but instead only tries to deal with getting the values of awaited calls.More Context
Task
-returning methods can actually cause exceptions too, even when not being awaited because theasync
keyword is what causes the swallowing, so if it is not present, the exceptions in the method will not be swallowed, such as the following.The reason why awaiting a call will actually
throw
the exception supposedly thown in aTask
-returningasync
method is because theawait
keyword is supposed to throw the swallowedException
s by design to make debugging easier in an asynchronous context.Important Note
The way that these "rewrites" are actually processed by the compiler and manifested by the compiled code may be different than how I have implied, but are roughly equivalent on a functional level.
Because, your methods are not executed asynchronously.
Execution will run synchronously until it "meet"
await
keyword.So in case of
void
application will throw an exception, because exception occurs in the current execution context.In case of
Task
even exception thrown synchronously it will be wrapped in theTask
and returned to the caller.You should get desired behaviour with
void
as well if you will useawait
in the function.TL;DR
This is because
async void
shouldn't be used!async void
is only there to make legacy code work (e.g. event handlers in WindowsForms and WPF).Technical details
This is because of how the C# compiler generates code for the
async
methods.You should know that behind
async
/await
there's a state machine (IAsyncStateMachine
implementation) generated by the compiler.When you declare an
async
method, a state machinestruct
will be generated for it. For yourex()
method, this state machine code will look like:Note that
this.builder.SetException(exception);
statement. For aTask
-returningasync
method, this will be anAsyncTaskMethodBuilder
object. For avoid ex()
method, it will be anAsyncVoidMethodBuilder
.The
ex()
method body will be replaced by the compiler with something like this:(and for the
async void ex()
, there will be no lastreturn
line)The method builder's
Start<T>
method will call theMoveNext
method of the state machine. The state machine's method catches the exception in itscatch
block. This exception should normally be observed on theTask
object - theAsyncTaskMethodBuilder.SetException
method stores that exception object in theTask
instance. When we drop thatTask
instance (noawait
), we don't see the exception at all, but the exception itself isn't thrown anymore.In the state machine for
async void ex()
, there's anAsyncVoidMethodBuilder
instead. ItsSetException
method looks different: since there's noTask
where to store the exception, it has to be thrown. It happens in a different way, however, not just a normalthrow
:The logic inside that
AsyncMethodBuilderCore.ThrowAsync
helper decides:SynchronizationContext
(e.g. we're on a UI thread of a WPF app), the exception will be posted on that context.ThreadPool
thread.In both cases, the exception won't be caught by a
try-catch
block that might be set up around theex()
call (unless you have a specialSynchronizationContext
that can do this, see e.g. Stephen Cleary'sAsyncContext
).The reason is simple: when we post a
throw
action or enqueue it, we then simply return from theex()
method and thus leave thetry-catch
block. Then, the posted/enqueued action is executed (either on the same or on a different thread).