A WPF button handler freezes forever on .Result. Find and fix the deadlock.
A WPF click handler calls an async method and blocks on the returned Task with .Result. The window freezes permanently: the continuation inside FetchAsync never runs, no exception is thrown, and CPU usage stays at zero.
Constraints: the handler must still assign the fetched text to Output.Text on the UI thread, and FetchAsync must stay async. Assume SynchronizationContext.Current on the click handler is the WPF dispatcher context.
private void OnClick(object sender, RoutedEventArgs e)
{
string data = FetchAsync().Result; // the UI freezes here
Output.Text = data;
}
private async Task<string> FetchAsync()
{
using var http = new HttpClient();
string body = await http.GetStringAsync("https://example.com/data");
return body.Trim();
}
Find and fix the bug.
.Result blocks the UI thread, while the await in FetchAsync captured the dispatcher's SynchronizationContext and posts its continuation back to that blocked loop. Fix it by going async all the way: await the task in an async handler.
- ✗Blaming the thread pool instead of the captured
SynchronizationContextthe continuation is posted to - ✗Swapping
.Resultfor.Wait()orGetAwaiter().GetResult()— all three block the same thread - ✗Adding
ConfigureAwait(false)to the handler rather than to the awaits inside the async method
- →Why would
ConfigureAwait(false)insideFetchAsyncalso unblock this, and why is it still the weaker fix? - →Why does the same code not deadlock in an ASP.NET Core controller?
The bug
.Result blocks the UI thread synchronously. But the await inside FetchAsync has already captured the WPF dispatcher's SynchronizationContext, so when the HTTP request completes it tries to run the continuation (body.Trim() and the return) on the UI message loop.
That message loop is parked inside .Result and cannot pump a single message. The continuation never runs → the task never completes → .Result never returns. It is a textbook two-party deadlock: the UI thread waits on the task, the task waits on the UI thread.
private void OnClick(object sender, RoutedEventArgs e)
{
string data = FetchAsync().Result; // ❌ blocks the UI thread and its message loop
Output.Text = data;
}
private async Task<string> FetchAsync()
{
using var http = new HttpClient();
// ❌ this await captures the dispatcher SynchronizationContext
string body = await http.GetStringAsync("https://example.com/data");
return body.Trim(); // ❌ this continuation can never be scheduled
}
The fix
Go async all the way. An event handler is the one legitimate place for async void: make it async and await the task instead of blocking on it.
private async void OnClick(object sender, RoutedEventArgs e) // ✅ async void is fine for a handler
{
string data = await FetchAsync(); // ✅ the UI thread is released and keeps pumping messages
Output.Text = data; // ✅ the continuation is posted back to the dispatcher
}
The handler leaves the UI thread for the duration of the request, the message loop keeps running, and when the task completes its continuation is posted to the dispatcher — so the Output.Text assignment still happens on the UI thread.
⚠️ Swapping .Result for .Wait() or GetAwaiter().GetResult() fixes nothing: all three block the same thread. ConfigureAwait(false) on the await inside FetchAsync would also unblock this particular call (the continuation goes to a pool thread), but that is only a library-side band-aid — the blocking caller still occupies the UI thread and remains the real bug.