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(); }
As we know, in asynchronous programming, control does not wait for the function's result and it executes the next line. So when the function throws an exception, at that moment the program control is out of the try-catch block.
A Task returning async method can be awaited, and when the task completes, the continuation of the task is scheduled to run. A void returning async method cannot be awaited; it is a "fire and forget" method. It does work asynchronously, and you have no way of telling when it is done.
The call to the async method starts an asynchronous task. However, because no Await operator is applied, the program continues without waiting for the task to complete. In most cases, that behavior isn't expected.
In short, if your async method is an event handler or a callback, it's ok to return void .
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).
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 machine struct
will be generated for it. For your ex()
method, this state machine code will look like:
void IAsyncStateMachine.MoveNext()
{
try
{
throw new Exception();
}
catch (Exception exception)
{
this.state = -2;
this.builder.SetException(exception);
}
}
Note that this.builder.SetException(exception);
statement. For a Task
-returning async
method, this will be an AsyncTaskMethodBuilder
object. For a void ex()
method, it will be an AsyncVoidMethodBuilder
.
The ex()
method body will be replaced by the compiler with something like this:
private static Task ex()
{
ExAsyncStateMachine exasm;
exasm.builder = AsyncTaskMethodBuilder.Create();
exasm.state = -1;
exasm.builder.Start<ExAsyncStateMachine>(ref exasm);
return exasm.builder.Task;
}
(and for the async void ex()
, there will be no last return
line)
The method builder's Start<T>
method will call the MoveNext
method of the state machine. The state machine's method catches the exception in its catch
block. This exception should normally be observed on the Task
object - the AsyncTaskMethodBuilder.SetException
method stores that exception object in the Task
instance. When we drop that Task
instance (no await
), 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 an AsyncVoidMethodBuilder
instead. Its SetException
method looks different: since there's no Task
where to store the exception, it has to be thrown. It happens in a different way, however, not just a normal throw
:
AsyncMethodBuilderCore.ThrowAsync(exception, synchronizationContext);
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 the ex()
call (unless you have a special SynchronizationContext
that can do this, see e.g. Stephen Cleary's AsyncContext
).
The reason is simple: when we post a throw
action or enqueue it, we then simply return from the ex()
method and thus leave the try-catch
block. Then, the posted/enqueued action is executed (either on the same or on a different thread).
Please read important note at bottom.
The async void
method will crash the application because there is no Task
object for the C# compiler to push the exception into. On a functional level, the async
keyword on a Task
-returning method is just heavy syntax sugar that tells the compiler to rewrite your method in terms of a Task
object using the various methods available on the object as well as utilities such as Task.FromResult
, Task.FromException
, and Task.FromCancelled
, or sometimes Task.Run
, or equivalents from the compiler's point of view. This means that code like:
async Task Except()
{
throw new Exception { };
}
gets turned into approximately:
Task Except()
{
return Task.FromException(new Exception { });
}
and so when you call Task
-returning async
methods that throw
, the program doesn't crash because no exception is actually being thrown; instead, a Task
object is created in an "excepted" state and is returned to the caller. As mentioned previously, an async void
-decorated method doesn't have a Task
object to return, and so the compiler does not attempt to rewrite the method in terms of a Task
object, but instead only tries to deal with getting the values of awaited calls.
Task
-returning methods can actually cause exceptions too, even when not being awaited because the async
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.
Task Except() // Take note that there is no async modifier present.
{
throw new Exception { }; // This will now throw no matter what.
return Task.FromResult(0); // Task<T> derives from Task so this is an implicit cast.
}
The reason why awaiting a call will actually throw
the exception supposedly thown in a Task
-returning async
method is because the await
keyword is supposed to throw the swallowed Exception
s by design to make debugging easier in an asynchronous context.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With