on
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:
- Read the MSDN documentation1
- Acknowledge the danger of async void2
- Bookmark this conference and follow it with concentration3
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 !");
}
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
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.");
}
}
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("============");
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);
}
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));
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));
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 await
4 :
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 async
5:
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:
await
is enclosed to anasync
method, also called theenclosing asynchronous operation
await
also represents anasynchronous operation
- when used,
await
suspends theenclosing asynchronous operation
until its properasynchronous operation
finished await
suspends itsenclosing asynchronous operation
but does not block the thread running itasync
is used to mark diverse constructs as being asynchronous compatible (method, lambda…)async
runs synchronously until it encounters anawait
operation- when
async
method encounters anawait
operation, it suspends its own execution untilawait
finished. - while suspending its execution the
async
method ‘returns’ the execution flow to the caller. ‘taking back’ the execution flow when theawait
operation finishes
Not appearing in the excerpts but also true:
await
shall only appear within anasync
method.await
shall only work on aTask
- Method marked with
async
should return aTask
orvoid
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);
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;
}
Is equivalent of writing:
public static Task SilentlyEffectCrashAsync() {
try {
return EffectCrashAsync();
} catch (Exception e) {
Console.WriteLine("Ignoring exception.");
}
return Task.CompletedTask;
}
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;
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:
- https://github.com/Nimamoh/AsyncAwaitPitfall
- Or just the main file: Program.cs
Knowing so, if you think you found an error or inexact content, you are more than welcome to notify it through comment below ⏬.
Also, if you found the content useful and it helped you, consider leaving a comment too or, better, give me fuel buying me a coffee with the link on the top of the website. 🙏