Thursday, June 25, 2015

Exceptions in async methods

I'm not going to go too far in to detailing how async/await work, you can find the documentation here. I am going to point out an easy to miss nuance about how exceptions are handled. 

Exceptions in an async method that is not awaited for DO NOT RETHROW, they just quietly disappear.

Mind you it's perfectly well documented in the above link but I find it important enough to draw special attention to it. Missing this small detail can lead to silent failures that can take hours to diagnose.


I present you with this code example.


static async Task DoWork()
{
    try
    {
        Console.WriteLine("About to do something that might throw");

        SomethingThatMightFail();

        Console.WriteLine("Moving along");
    }
    catch (Exception ex)
    {
        Console.WriteLine("An exception has occured!");
    }
}


Looks fine, right? If some exception happens inside SomethingThatMightFail it'll get caught by your valiant try catch and be handled as needed and you will move on with your life happily, right? WRONG. 


SomethingThatMightFail contains one line: 
throw new InvalidOperationException();

What does this code output if it runs?
About to do something that might throw
Moving along

Where's the exception? Why was it not caught? Because SomethingThatMightFail is an async method. Which brings up the first point:

Name your async methods appropriately!

SomethingThatMightFail() should be named SomethingThatMightFailAsync() so at least you or other developers know what they need to watch out for without having to check each method in a code block to see if maybe one of them is async and that's why they're getting a mysterious silent failure.

What makes this particularly dangerous is you won't know an exception occurred unless you happen to catch the "A first chance exception of type 'System.InvalidOperationException' occurred" in your Visual Studio output window. 
This unawaited async might be buried in a several hundred line block of somebody else's code. You'll know there's an error happening but won't be able to pin it down. You'll try to wrap it in a try catch but nothing will be caught. You'll try to step through the code but it will run start to finish. Five hours and 17 grey hairs later you'll hopefully notice that there's an unawaited async that's failing.

The best fix for this is simply to await the async method:
static async Task DoWork()
{
    try
    {
        Console.WriteLine("About to do something that might throw");

        await SomethingThatMightFailAsync();

        Console.WriteLine("Moving along");
    }
    catch (Exception ex)
    {
        Console.WriteLine("An exception has occured!");
    }
}

Now this happily works, and outputs the expected: 

About to do something that might throw
An exception has occured!


Note that you can also do

Task somethingThatMightFailAsyncTask = SomethingThatMightFailAsync();
//Do Other stuff
await somethingThatMightFailAsyncTask;

Or if you need more than one to run at the same time you can also do

Task somethingThatMightFailAsyncTask = SomethingThatMightFailAsync();
Task somethingElseThatMightFailAsyncTask = SomethingElseThatMightFailAsync();
Task.WaitAll(somethingThatMightFailAsyncTask, somethingElseThatMightFailAsyncTask);
 

As long as you're awaiting within the try catch it'll get caught.

It's important to note that your method must return Task or it cannot be awaited. Void's will not work.


The above is all you need in most cases.
Below is how to handle if you don't want to await.
You probably won't usually need this.

If you don't want to wait for the return of the method, but instead want to launch it and let it run on it's own in parallel you must build all exception handling into the method. Unless you await the exception will be confined to within your method and not rethrow. 

If you cannot modify the contents of the method (e.g. it's an external library) and do not want to await it in your main code, just to catch an exception in case it throws, you must create a wrapper method that will await and handle the exception.








static async Task DoWork()
{
    Console.WriteLine("Start doing work");
    WrapperForExternalAsync();
    Console.WriteLine("Keep doing work");
}

static async Task WrapperForExternalAsync()
{
    try
    {
        await ExternalAsync();
    }
    catch (Exception ex)
    {
        Console.WriteLine("Exception in async method");
    }
}

This will output
Start doing work
Exception in async method
Keep doing work

or (depending on the timing)
Start doing work
Keep doing work
Exception in async method


The DoWork method keeps going regardless of the result of the ExternalAsync, which now runs in parallel, but you do catch the error and can handle it as needed.






No comments:

Post a Comment