Why is .GetAwaiter().GetResult() bad in C#?

Why is .GetAwaiter().GetResult(), or .Wait() or .Result bad? It ends up boiling down to deadlocks and threadpool starvation. This post gives a gentle, high up look at why this may happen.

Why is .GetAwaiter().GetResult() bad in C#?
Three smashed together examples to steer clear of.

The Short:

  1. Deadlocks
  2. Threadpool Starvation

The Long

For the purposes of this not-so-in-depth post, consider the following the same evil:

  • .Wait()
  • .Result
  • .GetResult()

There is nuance between them but feel free to read about them on your own.

The Documentation Says No

GetAwaiter() returns a TaskAwaiter object and if we peek into the documentation it says:

"This type is intended for compiler use only."

💡
It used to say "This API supports the product infrastructure and is not intended to be used directly from your code." but was updated to the above. See Update TaskAwaiter.xml #7968 and linked discussions.

The Documentation Says No, Again

The GetResult() documentation has been updated since this post was originally written. But in the PR to update said documentation, Stephen Toub from Microsoft outlines more of the "No":

Now, there's the related question of "what about GetAwaiter().GetResult() on a Task rather than on a configured awaiter, as those docs also say the same thing". When all of this support was introduced, the intent was in fact that no one should ever be using these directly, that they were purely for compiler consumption.
💡
It used to say "This API supports the product infrastructure and is not intended to be used directly from your code." but was updated to the above. See Update TaskAwaiter.xml #7968 and linked discussions.

Deadlocks Can Happen

There are some very smart people with some very good answers but in short deadlocks happen because:

  1. The thread doing the work is now blocked due to a .Result and waiting for the call to come back.
  2. When the async task returns from the work, there's no thread to complete the work as it is stuck at .Result.

It's a little more complex due to SynchronizationContext and how .NET Core runs differently from an ASP.NET application which runs differently from a regular console application. Linking again, this fantastic writeup by Eke Péter goes much more in depth.

"But I've Never Seen a Deadlock Happen!"

Throw enough calls to simulate a high workload or call from a UI thread.

Threadpool Starvation

Now that we get the gist of how deadlocks happen, we can apply this to a threadpool. If we run enough load and have enough threads in the pool waiting for their own .Result calls, then eventually there will be no thread left to actually do the returned work.

Nope, same reasons as above. Sure in your weekend project it might be fine, but at 5:15pm on a Friday when there's a deadlock and you're trawling through error logs and memory dumps, maybe not. But if that's what you're into, I'm not shaming.

Alternatively if you'd like to make an HTTP request I'd like to think you have two options when you're currently using HttpClient in a synchronous codebase. I'm sure there are more, but these are easy to argue:

  1. Use another way to perform the HTTP request. Either a third party library or an older .NET way such as HttpWebRequest.
  2. Embrace the viral nature of async and let it propagate through your code.

The Quickest Example...

...With the least setup that I could get going. Credit to the example in this post that helped me a smidge.

  1. In Visual Studio, create a new MVC project
  2. Have your HomeController look like the code below
  3. Run
public class HomeController : Controller
    {
        public ActionResult Index()
        {
            var githubTask = GetGitHubStringAsync();
            var githubString = githubTask.Result.ToString();

            return View();
        }

        public ActionResult About()
        {
            ViewBag.Message = "Your application description page.";

            return View();
        }

        public ActionResult Contact()
        {
            ViewBag.Message = "Your contact page.";

            return View();
        }

        public static async Task<object> GetGitHubStringAsync()
        {
            using (var client = new HttpClient())
            {
                // Technically does a 403, but that doesn't matter for our case as all we need is a HTTP response
                var githubResult = await client.GetStringAsync(@"https://api.github.com/zen");
                return githubResult;
            }
        }
    }

The default index page should not load, and the browser will be left waiting.

To Finish

It's rough when you're trying to integrate an async piece of code into an existing (possibly legacy) synchronous codebase but I hope that this light brush of knowledge will help you understand why it can be scary to blindly throw around .GetAwaiter().GetResult() or any other async blocking call.

There is a lot more to dive into too, especially around SynchronizationContext and ConfigureAwait(false). Try a good bite into those if you're hungry for more understanding.

References:

Understanding Async, Avoiding Deadlocks in C#
You ran into some deadlocks, you are trying to write async code the proper way or maybe you’re just curious. Somehow you ended up here, and you want to fix a deadlock or improve your code. I’ll try…
davidfowl/AspNetCoreDiagnosticScenarios
This repository has examples of broken patterns in ASP.NET Core applications - davidfowl/AspNetCoreDiagnosticScenarios
Is Task.Result the same as .GetAwaiter.GetResult()?
I was recently reading some code that uses a lot of async methods, but then sometimes needs to execute them synchronously. The code does: Foo foo = GetFooAsync(...).GetAwaiter().GetResult(); Is t...
GetAwaiter().GetResult() vs GetAwaiter()?
Most devs are using GetAwaiter().GetResult(), and I can barely dig up examples using it with without the GetResult() part, however it is illogical to use GetResult() when we don’t need the result....