Pitfalls when writing asynchronous code in C# – async, await, and Task

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.


Logging utility

In the following examples I will use a simple logger:

C#

It will display the timestamp in seconds, thread ID and a message.

Async void

There are three signatures to declare an async function:

Note: I'll soon be sharing short, practical tips on Angular — a good way to pick up something new if you're interested.

  • 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:

C#

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:

C#

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:

C#

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:

C#

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:

C#

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:

C#

Option 2: get the awaiter:

C#

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.:

C#

Since C# 7.1, it is easier, as async Task Main() is allowed:

C#

Reference: C# 7.1

Read next

0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x