Beware mixing Tasks with async/await

I recently came across an issue within a pipeline of asynchronous method calls where an attempt to deserialize a response from json to a known type may throw an exception, usually when the response is not json. For example, this may be the case when using something like nginx as a proxy for authentication purposes and failed authentication returns a HTML page. A simple reproducible example would be

void Main()
{
    MainAsync().Wait();
}

async Task MainAsync()
{
    var result = await TryDeserializeAsync().ConfigureAwait(false);    
    Console.WriteLine(result);
}

Task<double?> TryDeserializeAsync()
{
    try { 
        return DeserializeAsync(); 
    }
    catch {
        // swallow any exceptions thrown.
    }
    return null;
}

async Task<double?> DeserializeAsync()
{
    await Task.Delay(100);
    throw new Exception("Serialization error");
}

Here we await TryDeserializeAsync() which in turn calls the actual DeserializeAsync() implementation, swallowing any exception that may occur in deserialization and simple returning null. That's the plan anyway. If we run this code (in LINQPad), what we actually get is

aggregation-exception

The problem here is line 15, in TryDeserializeAsync(); DeserializeAsync() is called and the result of the call to DeserializeAsync(), Task<double?>, is returned from TryDeserializeAsync(). There is no exception here since returning the Task<double?> does not throw an exception, it is in getting the result of the Task that the exception is thrown and the result is accessed when the Task is await’ed at line 8, in MainAsync(). Obvious when you think about it Smile

The solution to this particular problem is to await the DeserializeAsync() call in TryDeserializeAsync()

void Main()
{
    MainAsync().Wait();
}

async Task MainAsync()
{
    var result = await TryDeserializeAsync().ConfigureAwait(false);    
    Console.WriteLine(result);
}

async Task<double?> TryDeserializeAsync()
{
    try 
    { 
        return await DeserializeAsync().ConfigureAwait(false); 
    }
    catch 
    {
        // swallow any exceptions thrown.
    }
    return null;
}

async Task<double?> DeserializeAsync()
{
    await Task.Delay(100);
    throw new Exception("Serialization error");
}

This now swallows the exception thrown in DeserializeAsync and retuns null as expected.

The motto of the story here is that careful consideration should be taken when mixing the await’ing and non-await’ing of methods that return Task<T>; it was easy to spot the problem and fix in this case, but this may not be so for a more complex codebase. Stephen Cleary has a great article on best practices on Asynchronous Programming on MSDN as well as an invaluable blog that covers many of the aspects of asynchrony in C#/.NET.

Comments

comments powered by Disqus