Pitfalls when writing asynchronous code in C# – async, await, and Task
Today I’ll write about the basics of writing asynchronous code in C#. Because it is hard to find and fix bugs in asynchronous or multi-thread code, you ought to know the rules mentioned below to avoid common mistakes.
Table of contents
Logging utility
In the following examples I will use a simple logger:
It will display the timestamp in seconds, thread ID and a message.
Async void
There are three signatures to declare an async function:
By the way, I’ve put together a small collection of tools I use regularly — things like time tracking, Git helpers, database utilities, and a few others that make development a bit smoother.
async void
async Task
async Task<T>
The first one should be used only for entry methods, like event handlers, and it is not an asynchronous call. The caller will not wait for the completion of the method.
Check the following example:
It’s not asynchronous
Method RunVoidAsync
is marked with async void
to demonstrate the issue. First, it is invalid to write await RunVoidAsync()
since it does not return a Task
.
Second, the calling thread does not wait until the completion of RunVoidAsync
:
00s (#1): Start 00s (#1): Void Start 00s (#1): End 02s (#4): Void End
The method first runs synchronously, but once the await
keyword starts a new task, the execution stops and returns to the caller, which goes on without waiting for RunVoidAsync
to complete. Then, after the await
finishes two seconds later, the execution of RunVoidAsync
continues in a new thread (#4).
Its errors are not caught
The next problem with async void
is that the exceptions thrown in that async method will not be immediately caught by the caller:
The exception wasn’t caught by the caller, as can be seen in the output:
00s (#1): Start 00s (#1): Throw Void Start 00s (#1): End Unhandled exception: System.Exception: OOPS
How to handle the exception?
One way is to use AppData.UnhandledException
:
You will get the information about the error, but the application will crash anyway. I thought it would happen only in Visual Studio debugging or in Debug configuration, but the same happened when I executed the application in Release mode in a console.
00s (#1): Start 00s (#1): Throw Void Start 00s (#1): End 00s (#3): Global error: System.Exception: OOPS ... Unhandled exception: System.Exception: OOPS
There are other similar methods you can try in other environments:
AppDomain.CurrentDomain.UnhandledException
AppDomain.CurrentDomain.FirstChanceException
Application.ThreadException
(WinForms, docs)Application_Error
(ASP.NET)Dispatcher.UnhandledException
(docs)Application.DispatcherUnhandledException
(WPF, docs)TaskScheduler.UnobservedTaskException
(docs)- even more on the topic
Async Task
The async Task
and async Task<T>
signatures differ only by the return type. The first type of method will not return information (it’s similar to a synchronous public void Method()
method) and the second type of method will return T
value or object (it’s similar to a synchronous public T Method()
method).
Async methods will execute in another thread if necessary, but the caller will not continue until the called method finishes. Follow this example:
Now the execution is predictable, yet it does not block the main (calling) thread:
00s (#1): Start 00s (#1): Void Start 02s (#4): Void End 02s (#4): End
Note that the new thread (#4) continued execution not only in RunTaskAsync()
, but also continued in Main()
.
If you wanted to return some data from RunTaskAsync()
, e.g. a string
, then change the return type of that method to Task<string>
.
Forgot to await
By the way, were you wondering what will happen if you forget to await an async method? The result after changing await RunTaskAsync();
to RunTaskAsync();
is the same as when calling an async void
method:
00s (#1): Start 00s (#1): Void Start 00s (#1): End 02s (#4): Void End
And the result of replacing await Task.Delay(2000);
with Task.Delay(2000);
is like there was no delay; the delay will be called and invoked in another thread, but the application will just go on, since the delay is not awaited:
00s (#1): Start 00s (#1): Void Start 00s (#1): Void End 00s (#1): End
In this particular case, you can use Thread.Sleep
to make some delay – but it is not asynchronous programming, it will block the thread.
Exceptions
Another benefit of using the correct signature is the ease of catching exceptions thrown in the asynchronous method:
Result:
00s (#1): Start 00s (#1): Throw Void Start 00s (#1): Caught: OOPS 00s (#1): End
Convention
You could see above what happens when you forget to await
an asynchronous method. Therefore it’s a good idea to follow the convention to end the names of the async methods with “Async”, e.g. public async Task RunAsync()
instead of public async Task Run()
. It will work as a reminder to put await
in front of such methods when you’ll be calling them.
You will get a warning
warning CS4014: Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the 'await' operator to the result of the call.
nonetheless, but why wait until compilation or why check called method’s source code to be sure whether to await or not?
Call an asynchronous method from a synchronous method
Option 1: get the task and wait for the task to complete, blocking the main thread:
Option 2: get the awaiter:
The result is the same for both approaches:
00s (#1): Start 02s (#1): Res = 2 02s (#1): End
Async Main
Prior to C# 7.1, it was necessary to make workarounds to call an asynchronous method from Main()
, e.g.:
Since C# 7.1, it is easier, as async Task Main()
is allowed:
Reference: C# 7.1