Async Traps

Async programming in C# is something that's been covered in detail by a number of people (google Brandon Minnick or Filip Ekberg), but it's still something I see misused on a regular basis, so here is a very brief overview of a couple of traps to avoid.

Consider the following two async methods, identical except for the return type (and the name):

private async void VoidAsync()
{
    try
    {
        int a = 9;
        int b = 0;
        int c = a / b;
    }
    catch(Exception ex)
    {
        if (ex is DivideByZeroException)
        {
            string well = "whaddya know?";
            throw ex;
        }
    }
}
private async Task TaskAsync()
{
    try
    {
        int a = 9;
        int b = 0;
        int c = a / b;
    }
    catch (Exception ex)
    {
        if (ex is DivideByZeroException)
        {
            string well = "whaddya know?";
            throw ex;
        }
    }
}

Both will throw a DivideByZeroException when called.
Both will successfully catch, and then re-throw that exception.

Now let's look at three different ways of calling these methods, and the three different outcomes they cause.

  1. From inside an async method (which CAN be an async void method such as an event handler), await the TaskAsync method from inside a try..catch.
private async void awaitedTaskButtonClick(object sender, EventArgs e)
{
    try
    {
        await TaskAsync();
    }
    catch(Exception ex)
    {
        if (ex is DivideByZeroException)
        {
            string oh = "dear!";
        }
    }
}

Result: The exception will be caught correctly.

  1. Call TaskAsync, but don't await it. Call it from inside a try..catch.
private void unawaitedTaskButtonClick(object sender, EventArgs e)
{
    try
    {
        TaskAsync();
    }
    catch (Exception ex)
    {
        if (ex is DivideByZeroException)
        {
            string not = "again!";
        }
    }
}

Result: The re-thrown exception will not be caught. Execution of the calling code will continue as though nothing had gone wrong.

  1. Call VoidAsync (obviously not awaited) from inside a try..catch.
private void voidButtonClick(object sender, EventArgs e)
{
    try
    {
        VoidAsync();
    }
    catch(Exception ex)
    {
        if (ex is DivideByZeroException)
        {
            string never = "mind!";
        }
    }
}

Result: The re-thrown exception will bring your app down. During debug, an error will be shown stating that an unhandled exception has occurred, despite it looking like it's inside a try..catch.

With async methods, exceptions that are raised are attached to the associated Task object. The behaviour above that you perhaps weren't expecting arises when there is no Task object.

If you want to dig a bit deeper, this early post covers the fundamentals:

https://msdn.microsoft.com/en-us/magazine/jj991977.aspx

Brandon gives a great presentation on it here, covering what's happening under the hood and giving more insight into why the exception handling is the way it is:

https://channel9.msdn.com/Shows/On-NET/Brandon-Minnick-asyncawait-best-practices

Happy coding.