Task.Result and .Wait() Deadlocks in C#: Why Sync-over-Async Hangs

Calling .Result or .Wait() on a Task looks like a quick way to use an async method from synchronous code. In a desktop app it can freeze the UI forever. In ASP.NET Core it ties up thread-pool threads and slows everything down under load. This guide explains why "sync over async" goes wrong and how to remove it.

Video explainer (1:47) · Watch with transcript

How the classic deadlock happens

Classic ASP.NET (System.Web) has the same kind of context and the same deadlock. ASP.NET Core has no SynchronizationContext, so it usually does not deadlock, but every blocked call still holds a thread-pool thread for the whole wait, which leads to thread-pool starvation under load.

  1. The UI thread calls GetDataAsync().Result and blocks, waiting for the task
  2. Inside GetDataAsync, an await finishes and wants to continue on the UI thread (the captured SynchronizationContext)
  3. The UI thread is blocked waiting for the task, so the continuation can never run
  4. The task never completes, and the app hangs

Before: blocking on async code

public IActionResult GetCustomer(int id)
{
    var customer = _service.GetCustomerAsync(id).Result;
    return Ok(customer);
}

After: async all the way

public async Task<IActionResult> GetCustomer(int id, CancellationToken ct)
{
    var customer = await _service.GetCustomerAsync(id, ct);
    return Ok(customer);
}

If you really cannot await

Sometimes an interface or framework callback is synchronous and you cannot change it. Options, from best to worst:

  • Change the interface to return Task. This is almost always worth the refactor.
  • Use a synchronous API if the library offers one (many I/O libraries do).
  • In library code, use ConfigureAwait(false) on every await so continuations do not need the caller's context. This prevents the UI deadlock but does not stop the thread from blocking.
  • As a last resort, GetAwaiter().GetResult() instead of .Result. It throws the original exception instead of an AggregateException, but it still blocks.

Related traps

  • async void methods: exceptions cannot be caught by the caller; use async Task except for event handlers
  • Task.Run(() => SomethingAsync()).Result: still blocks, just on a different thread
  • Parallel.ForEach with async lambdas: the lambdas become async void; use Parallel.ForEachAsync instead

Find .Wait() and .Result calls automatically

CodeInspector flags blocking waits on tasks, both .Wait() and .Result, as rule CI0006 across the repository, with the exact file and line. Scan your project at https://codeinspector.app; new accounts get 100 free credits.

Related