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
- ConfigureAwait(false): the rest of
LoadAsyncruns on a pool thread, so.Resultreturns. No deadlock, but the UI thread was frozen for the whole delay. - await:
OnClickawaits instead of blocking. The UI thread is free during the delay, and the handler resumes on it afterwards.
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