async Task Main()
{
try
{
Operations.TidyCache("");
Console.WriteLine("Completed Successfully");
}
catch (Exception ex)
{
Console.Error.WriteLine($"Critical error: {ex.Message}"); //this try-catch will not be entered due to lack of awaiting Operations.TidyCache
}
}
public static class Operations {
public static async void TidyCache(string key){
ArgumentException.ThrowIfNullOrEmpty(key);
await Task.Run(async() =>
{
await System.Threading.Tasks.Task.Delay(500);
Console.WriteLine("Finished running async task : waited half a second above");
});
}
}
Output from code above :Completed Successfully
ArgumentException (...)
The value cannot be an empty string. (Parameter 'key')
The problem with the code shown above is that although we do an initial error of passing in an empty string as the parameter key to method TidyCache, our try-catch will not be actually catched. The reason for this is that our async method is called and entered and the ArgumentException is thrown inside the async state machin and therefore thrown outside the try/catch. The end result is that since we do not await the method either, the caller will not get the chance to catch the exception and the exception is thrown directly on the thread pool and can also in some applications lead to application crash. We can move the try catch inside the TidyCache method and let the async method return Task instead of void. The calling code uses a discard assignment.
First solution
async Task Main()
{
try
{
_ =Operations.TidyCache("");
Console.WriteLine("Completed Successfully");
}
catch (Exception ex)
{
Console.Error.WriteLine($"Critical error: {ex.Message}"); //this try-catch will not be entered due to lack of awaiting Operations.TidyCache
}
}
public static class Operations
{
public static async Task TidyCache(string key)
{
try
{
ArgumentException.ThrowIfNullOrEmpty(key);
await Task.Delay(500);
Console.WriteLine("Finished running async task");
}
catch (Exception ex)
{
Console.Error.WriteLine($"TidyCache failed: {ex.Message}");
}
}
}
Output from the code above : TidyCache failed: The value cannot be an empty string. (Parameter 'key')
Completed Successfully
Second solution
The first solution is the correct pattern for fire and forget pattern. The second solution shown below is the right way to handle exception for async methods by allowing the async task to flow its exception up to the caller and actually await the async method.
async Task Main()
{
try
{
await Operations.TidyCache("");
Console.WriteLine("Completed Successfully");
}
catch (Exception ex)
{
Console.Error.WriteLine($"Critical error: {ex.Message}"); //this try-catch will not be entered due to lack of awaiting Operations.TidyCache
}
}
public static class Operations {
public static async Task TidyCache(string key){
ArgumentException.ThrowIfNullOrEmpty(key);
await Task.Run(async() =>
{
await System.Threading.Tasks.Task.Delay(500);
Console.WriteLine("Finished running async task : waited half a second above");
});
}
}
Output from code above :Critical error: The value cannot be an empty string. (Parameter 'key')
The second solution shows the behavior and execution flow most programmers will expect from the code. The exception inside the async method that is thrown is catched in the calling code. Finally some (block) quotes about async void methods:
“async void is where exceptions go to die.”
“async void is the ghost of a Task — it haunts your runtime.”
“If you think you need async void, you probably need therapy.”
“async void is like fire-and-forget — except you forget the fire.”
“Use async void when you want exceptions to bypass your try/catch like a VIP.”