Async await through C#: a tricky pitfall explained and some advice for beginners

Programming with an asynchronous operation is always trickier than a classic syncrhonous sequential style. It’s a no news.

C# offers the async await construct. While it is quite simple to use, as a developper you must be careful with it for it plays heavily with the execution flow.

The wisest way to go about it is to take some time to understand it thoroughly and foresee the common pitfalls. I advise you to do the following before using it:

A crappy introduction

Let me tell you a story about a developper and his habits. A creature who takes pride in writing concise code and feels shame when he produces circumvoluted designs. A story of an everyday, regular, normal dev.

Our guy being originally a java developer, he felt like giving a go writing some C# for one of his projects.

I will simplify his thoughts for your comfort, hence these examples are just examples, do not try this on a production project

Who likes writing try catch?

An old habit of our friend is to wrap his error handling within simple constructs, so that instead of repeating try { ... } catch { ... } like this:

try {
    VeryCleverComputationThatNobodyUnderstand();
} catch(Exception) {
    Console.WriteLine("I am too talented, we do not need those pesky exceptions, let's ignore it !");
}
Code Snippet 1: Example of try catch

He prefers to write repetitive constructs like this:

var anInteger = Silently(ComputeAValueInAComplicatedManner(), 0); // We get a value no matter what
Silently(SideEffectWhichCouldGoWrongInAlotOfWays()); // Succeed or fail, I don't mind
Code Snippet 2: Construct using Silently
public static T Silently<T>(Func<T> expression, T fallback)
{
    try
    {
        return expression();
    }
    catch (Exception)
    {
        Console.WriteLine("Ignoring exception.");
    }

    return fallback;
}

public static void Silently(Action statement)
{
    try
    {
        statement();
    }
    catch (Exception)
    {
        Console.WriteLine("Ignoring exception.");
    }
}
Code Snippet 3: Implementation of Silently

This routine code has been a good companion of our fellow developer for quite some time now, rarely failed him and when used with frugality, is quite an awesome pattern!

Different ways to say the same thing

Elaborating on our last pattern, let’s consider our friend’s code:

int TwoMayCrash(bool crash = true)
{
    if (crash)
        throw new InvalidOperationException();
    return 2;
}

int Two() => TwoMayCrash(false);
int TwoCrash() => TwoMayCrash();

Action Effect = () => Console.WriteLine("Computed effect");
Action EffectCrash = () => throw new InvalidOperationException();

...

Console.WriteLine("============ Simple expressions.");
Assert(Silently(Two, 0) == 2, "Most basic example, should return 2");
Assert(Silently(TwoCrash, 0) == 0, "Here, the expression fails, we fallback.");
Console.WriteLine("============");

Console.WriteLine("============ Simple effects.");
Silently(Effect); // Print that effect took place
Silently(EffectCrash); // Print that effect failed, silently ignore the exception
Console.WriteLine("============");
Code Snippet 4: Base for next examples.

Nothing too surprising I hope. Now that we are familiarized with this, let’s try to compose it with some asynchronous code

With asynchronous code

Let’s consider this:

async Task<int> TwoAsync()
{
    return await Task.Run(Two);
}

async Task<int> TwoCrashAsync()
{
    return await Task.Run(TwoCrash);
}

async Task EffectAsync()
{
    await Task.Run(Effect);
}

async Task EffectCrashAsync()
{
    await Task.Run(EffectCrash);
}
Code Snippet 5: Base with asynchronous constructs.

Now, our friend have a way to compute the really resource hungry value of 2 and Console.WriteLine asynchronously ( ͡° ͜ʖ ͡°)

Composition with Silently

Now comes the time that require your attention. The following examples are flawed and don’t work as expected even though it appear to do so. Let’s begin:

await Silently(EffectAsync, Task.CompletedTask);
int two = await Silently(TwoAsync, Task.FromResult(0));
Code Snippet 6: Effect silently with async await

It behaves as expected, our friend sees Computed effect printing on the console and gets the value 2. The computation runs and we even fallback to a completed task if anything goes wrong (does it work as expected though ? Let’s pretend so)

Speaking of something going wrong, let’s try these ones:

await Silently(EffectCrashAsync, Task.CompletedTask);
int two = await Silently(TwoCrashAsync, Task.FromResult(0));
Code Snippet 7: Crash silently with async await

Our friend should not get anything printed on the console, retrieve 0 as a fallback result and should not get any crash. Really ?

In fact, he gets an uncatched InvalidOperationException instead. But why ?! (ノಥ益ಥ)ノ彡┻━┻)

The world collapses, here is why

What can he do if we cannot even trust a good old try {} catch(Exception) {} ! Let’s think this through with him.

This has to do with asynchronous computation obviously. We might be using the async await construct incorrectly. Let’s review it:

MSDN documentation says the following about await4 :

The await operator suspends evaluation of the enclosing async method until the asynchronous operation represented by its operand completes. When the asynchronous operation completes, the await operator returns the result of the operation, if any. When the await operator is applied to the operand that represents already completed operation, it returns the result of the operation immediately without suspension of the enclosing method. The await operator doesn’t block the thread that evaluates the async method. When the await operator suspends the enclosing async method, the control returns to the caller of the method.

And this about async5:

Use the async modifier to specify that a method, lambda expression, or anonymous method is asynchronous. If you use this modifier on a method or expression, it’s referred to as an async method. […] An async method runs synchronously until it reaches its first await expression, at which point the method is suspended until the awaited task is complete. In the meantime, control returns to the caller of the method, as the example in the next section shows.

Put differently:

Not appearing in the excerpts but also true:

async void is allowed to be compatible with event handling. It is generally strongly discouraged to use it because it can lead to the same kind of problems we are having now (though async void is not the cause in our case). This article2 address the async void issue in a more detailed manner.

Knowing these elements, we could picture async await as a chain. A chain we should not be breaking since it acts heavily with the execution flow of our program. We have a lead, let’s see if the chain is broken.

Let’s review our problematic call

await Silently(EffectAsync, Task.CompletedTask);
Code Snippet 8: Silently is problematic

Knowing the definition of silently:

public static T Silently<T>(Func<T> expression, T fallback)
{
    try
    {
        return expression();
    }
    catch (Exception)
    {
        Console.WriteLine("Ignoring exception.");
    }

    return fallback;
}
Code Snippet 9: Implementation of Silently.

Is equivalent of writing:

public static Task SilentlyEffectCrashAsync() {
    try {
        return EffectCrashAsync();
    } catch (Exception e) {
        Console.WriteLine("Ignoring exception.");
    }
    return Task.CompletedTask;
}
Code Snippet 10: Equivalent with try catch.

But wait a minute, EffectCrashAsync is an async task which is not await ed, nor is the call encapsulated in an enclosing asynchronous operation thanks to async keyword !

That’s it. We broke the chain when we encapsulate our async operation in Silently. What happens in this case then? We actually performed a fire and forget call. equivalent to this:

try {
    return Task.Run(() => new InvalidOperationException());
} catch(Exception e) {
    Console.WriteLine("Ignoring exception.");
}
return Task.CompletedTask;
Code Snippet 11: Equivalent fire and forget.

What we try { ... } catch { ... } was actually the ‘launching’ of the task, not its proper execution, thus the exception happening in the execution of the task is unhandled ! Without compiler warning !

Conclusion

We leave the reader the freedom to find how to fix the issue, it’s not the point of the article ;)

Asynchronous programming is always trickier than the synchronous and sequential world. The async await tool is actually great for writing more concise code. The only piece of advice I would give is to fully comprehend this construction before using it in a serious project.

Here is the source code illustrating the article if you want to tinker with it:


  1. Programmation asynchrone en C# | Microsoft Docs ↩︎

  2. Removing async void | John Thiriet ↩︎

  3. Correcting Common Async/Await Mistakes in .NET - Brandon Minnick - YouTube ↩︎

  4. await operator - C# reference | Microsoft Docs ↩︎

  5. async - Référence C# | Microsoft Docs ↩︎