Demystifying Async & Await in C#

You might have come across the two keywords async and await many times while working on .NET projects. Although it’s a fascinating thing to use, it’ll also be beneficial in knowing what happens under the hood when dealing with such code.

Before getting bogged down with in-depth details what’s what, we need to clarify some common misconceptions about Tasks and Threads used in C#.

🤔 Tasks Vs Threads

Simply put, Tasks are not Threads. If you have ever worked with Promises in other languages such as JavaScript, you will be quite comfortable in using them. Tasks are essentially Promises, which would be completed in a later point in time which lets you deal with the result returned from an asynchronous action. This also means that Tasks can be faulted and queried to know whether they are completed or not. Threads, on the other hand, are a more lower-level implementation of OS-level code executions.

It is really important to understand that tasks are not an abstraction over threads, and we should think of tasks as an abstraction over some work that’s intended to be happening asynchronously. In summary:

Tasks are not Threads
Tasks are similar to Promises providing you with a clean API to handle async code
Tasks do not guarantee parallel execution
Tasks can be explicitly requested to run on a separate thread via the Task.Run API.
Tasks are scheduled to be run by a TaskScheduler

🔮 The Magic of the ‘await’ keyword

As Stephen Cleary clearly mentions in his excellent blog post, async keyword only enables await. So, an async method would simply run like any other synchronous method until it sees an await keyword.

The await keyword is where the magic happens. It gives back control to the caller of the method that performed await and it ultimately allows an IO-bound (eg. calling a web service) or a CPU bound task (eg: such as a CPU-heavy calculation) to be responsive. All async and await does is providing us with some nice syntactic sugar to write cleaner code.

Let’s consider the following simple example:

public async Task<string> DownloadString(string url)
var client = new HttpClient();
var request = await client.GetAsync(url);
var download = await request.Content.ReadAsStringAsync();
return download;

The same code can be equivalent to what gets unfolded into under the hood:

public Task<String> DownloadString(string url)
var client = new HttpClient();
var request = client.GetAsync(url);
var download = request.ContinueWith(http =>
return download.Unwrap();

As you can see, it pretty much creates a chain of tasks that needs to execute and gets queued up with the TaskScheduler. The above code is somewhat okay, but it is also beneficial to always use the language constructs provided by C# without doing everything manually!

The sequence of how this gets executed is as follows:

Calling client.GetAsync(url) will create a request behind the scenes by calling lower-level .NET libraries.
Some part of its underlying code may run synchronously until it delegates its work from the networking APIs to the OS
At this point, a Task gets created and gets bubbled up to the original caller of the asynchronous code. This is still an unfinished Task!
During this time, the caller may query the status of the Task
Once the network request is completed by the OS level, the response gets returned via an IO completion port and CLR gets notified by a CPU interrupt that the work is done
The response gets scheduled to be handled by the next available thread to unwrap the data
The remainder of your async method continues running synchronously

The key takeaway here is that there won’t be any dedicated threads to complete a Task. Which also means that your task continuation/completion isn’t guaranteed to run on the same thread that started it.

☠️ Common gotchas when using async and await

You would be tempted to parallelise everything that you see in your code when you start using async and await (well, I did 😁). But there are some pitfalls that you should avoid in your code.

Making methods void methods async void

This is the first mistake I did when I got my hands dirty with async-await! I had a void method which called an async method within itself. Since you can’t use await without making the calling method to be async, I made the caller method async void !

public async void GetResult()
await SomeAsyncMethod();

The problem here is, the caller of the GetResult method doesn’t have any kind of control over the result of SomeAsyncMethod() This also causes other side-effects such as lack of a call stack to debug if something goes wrong, application crashes if an exception occurs etc.

However, there are times that you can’t avoid this though. If you open up AsyncWpfApp in our code example you would find code like the one below:

private async void AsyncParallelBtn_Click(object sender, RoutedEventArgs e)
var output = await RunAsyncParallelDemo.Start();

It’s not necessarily a problem if you find code like the one above since you can’t change the method signatures of the event handlers in WPF.

Nevertheless, it’s recommended to avoid using async void where possible.

Ignoring or Forgetting to ‘await’

It is an easy mistake to forget to await on an async task since the compiler would happily compile your code without complaining. In the below example we simply call an async function without awaiting on it.

public void Caller()

// Deliberately forgetting to await


DoSomeBackgroundWorkAsync() method will return a Task (either a Task or a Task depending on the implementation) to the caller method without actually executing it.

Therefore, be mindful when you are dealing with async methods (especially with third party libraries) not to forget to use await

Blocking async tasks with .Result() and .Wait()

This is another common trap developers jump into (unknowingly 😋) when they need to run some async code in a synchronous method. Let’s consider the below example:

public void DoSomeWork()
var result = DoAsyncWork().Result();

At a glance, it may look pretty convenient to use the .Result() method. However, this could cause serious deadlock problems. In order to avoid this, you should usually make your caller method async. This could be lot of work as it creates a cascading effect on your changes. But that’s usually preferable.

More ways to write non-blocking code as clearly depicted in MSDN:

It’s recommended to make the caller async where you would end up making a lot of cascading changes to your codebase although it could be painful if you are working on a legacy project.


I hope you enjoyed this article as much as I did writing it. The bottom line is that Task is almost always the best option; it provides a much more powerful API and avoids wasting OS threads.

Ready for more stuff? Head over to my Github repo to work through some sample code.

Suggestions or Found a Bug?

If you think there’s a misunderstanding in any of the things I explained, please feel free to comment down below 🙂 Also, if you found a bug or have any suggestions, please open a pull request in my Github repo. Cheers!


Flatlogic Admin Templates banner

Leave a Reply

Your email address will not be published. Required fields are marked *