The .Result deadlock on a UI thread

Read the explanation
What if

A click handler blocks the UI thread in .Result. The rest of LoadAsync needs that same thread, so neither can go on.

Predict first

A click handler calls LoadAsync().Result on the UI thread, and LoadAsync awaits a 100 ms delay. What happens?

0123456789clickUI thread: LoadAsync() until awaitLoad.Result: blocked foreverrest of LoadAsync: waits for the UI thread

Tap any block to see what that thread is doing.

  • Running
  • Waiting / parked
Step 1/5

A click handler runs on the UI thread, the one thread that runs all UI work, in order.

1public static async Task<string> LoadAsync()2{3    await Task.Delay(100); // the rest of LoadAsync is posted back to the UI thread4    return "data";5}6 7public static void OnClick() // runs on the UI thread8{9    Console.WriteLine("click");10    string data = LoadAsync().Result; // blocks the UI thread until LoadAsync is done11    Console.WriteLine($"got {data}");12}

Explanation

What just happened

The click handler ran on the UI thread and called LoadAsync().Result. LoadAsync ran until its await, which remembered the UI thread’s SynchronizationContext. When the delay ended, the rest of LoadAsync was posted to the UI thread’s queue. But the UI thread was blocked in .Result, waiting for exactly that code. Our test finds it stuck in WaitSleepJoin, forever.

Try the what-if switch

Rule of thumb

Async all the way: await instead of .Result or .Wait(). In library code, add ConfigureAwait(false) so a caller who does block can’t deadlock you.

Watch out

Our UI thread is a small stand-in for WPF or WinForms: one thread running everything posted to its SynchronizationContext, in order. That’s the part that matters here.

Common questions

Why does .Result or .Wait() deadlock?

On a thread with a SynchronizationContext, like a UI thread, await posts the rest of the method back to that thread. If that thread is blocked in .Result, the rest can never run, and .Result waits forever.

Does ConfigureAwait(false) fix the deadlock?

It avoids this one: the rest of the method runs on a thread-pool thread instead, so .Result returns. The UI thread is still blocked the whole time, so the app freezes until then.

Why doesn't my console app deadlock?

A console app has no SynchronizationContext, so the code after await runs on the thread pool. The same .Result returns fine there.

Verified: this timeline matches a real program, checked in CI on .NET 8 and 10. How we verify. Unclear or wrong? Tell us