Taken from article on async await by Stephen Cleary:
Figure 2 Exceptions from an Async Void Method Can’t Be Caught with Catch
private async void ThrowExceptionAsync()
{
throw new InvalidOperationException();
}
public void AsyncVoidExceptions_CannotBeCaughtByCatch()
{
try
{
ThrowExceptionAsync();
}
catch (Exception)
{
// The exception is never caught here!
throw;
}
}
... 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...
What does that actually mean? I wrote an extended example to try and glean more info. It has the same behaviour as Figure 2:
static void Main()
{
AppDomain.CurrentDomain.UnhandledException += (sender, ex) =>
{
LogCurrentSynchronizationContext("AppDomain.CurrentDomain.UnhandledException");
LogException("AppDomain.CurrentDomain.UnhandledException", ex.ExceptionObject as Exception);
};
try
{
try
{
void ThrowExceptionVoid() => throw new Exception("ThrowExceptionVoid");
ThrowExceptionVoid();
}
catch (Exception ex)
{
LogException("AsyncMain - Catch - ThrowExceptionVoid", ex);
}
try
{
// CS1998 C# This async method lacks 'await' operators and will run synchronously.
async void ThrowExceptionAsyncVoid() => throw new Exception("ThrowExceptionAsyncVoid");
ThrowExceptionAsyncVoid();
}
// exception cannot be caught, despite the code running synchronously.
catch (Exception ex)
{
LogException("AsyncMain - Catch - ThrowExceptionAsyncVoid", ex);
}
}
catch (Exception ex)
{
LogException("Main", ex);
}
Console.ReadKey();
}
private static void LogCurrentSynchronizationContext(string prefix)
=> Debug.WriteLine($"{prefix} - " +
$"CurrentSynchronizationContext: {SynchronizationContext.Current?.GetType().Name} " +
$"- {SynchronizationContext.Current?.GetHashCode()}");
private static void LogException(string prefix, Exception ex)
=> Debug.WriteLine($"{prefix} - Exception - {ex.Message}");
Debug output:
Exception thrown: 'System.Exception' in ConsoleApp3.dll
AsyncMain - Catch - ThrowExceptionVoid - Exception - ThrowExceptionVoid
Exception thrown: 'System.Exception' in ConsoleApp3.dll
An exception of type 'System.Exception' occurred in ConsoleApp3.dll but was not handled in user code
ThrowExceptionAsyncVoid
AppDomain.CurrentDomain.UnhandledException - CurrentSynchronizationContext: -
AppDomain.CurrentDomain.UnhandledException - Exception - ThrowExceptionAsyncVoid
The thread 0x1c70 has exited with code 0 (0x0).
An unhandled exception of type 'System.Exception' occurred in System.Private.CoreLib.ni.dll
ThrowExceptionAsyncVoid
The program '[18584] dotnet.exe' has exited with code 0 (0x0).
I want more details
async void
(with no await
) and void
CS1998 C# This async method lacks 'await' operators and will run synchronously.
await
, why is it behaving differently from simply void
?async Task
with no await
also behave differently from Task
?async void
and async Task
. Is a Task
object really created under-the-hood for async void
as suggested here?Edit. To be clear, this isn't a question about best practices - it is a question about compiler / runtime implementation.
If there is no current synchronization context (as in my example), where is the exception raised?
By convention, when SynchronizationContext.Current
is null
, that's really the same as SynchronizationContext.Current
equal to an instance of new SynchronizationContext()
. In other words, "no synchronization context" is the same as the "thread pool synchronization context".
So, the behavior you're seeing is that the async
state machine is catching the exception and then raising it directly on a thread pool thread, where it cannot be caught with catch
.
This behavior seems odd, but think about it this way: async void
is intended for event handlers. So consider a UI application raising an event; if it is synchronous, then any exceptions get propagated to the UI message processing loop. The async void
behavior is intended to mimic that: any exceptions (including ones after await
) are re-raised on the UI message processing loop. This same logic is applied to the thread pool context; e.g., exceptions from your synchronous System.Threading.Timer
callback handler will be raised directly on the thread pool, and so will exceptions from your asynchronous System.Threading.Timer
callback handler.
If it runs synchronously with no await, why is it behaving differently from simply void?
The async
state machine is handling the exceptions specially.
Does async Task with no await also behave differently from Task?
Absolutely. async Task
has a very similar state machine - it catches any exceptions from your code and places them on the returned Task
. This is one of the pitfalls in eliding async
/await
for non-trivial code.
What are the differences in compiler behaviour between async void and async Task.
For the compiler, the difference is just how exceptions are handled.
The proper way to think about this is that async Task
is a natural and appropriate language development; async void
is a weird hack that the C#/VB team adopted to enable asynchronous events without huge backwards-compatibility issues. Other async
/await
-enabled languages such as F#, Python, and JavaScript have no concept of async void
... and thus avoid all of its pitfalls.
Is a Task object really created under-the-hood for async void as suggested here?
No.
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