Quick answer: Never call .Result or .Wait on a Task from Unity’s main thread. Make the calling method async and use await. For zero-allocation main-thread async, use UniTask.

A loading screen calls an async data-fetcher with var data = FetchAsync().Result;. The game freezes immediately on this line and never recovers. The Task never completes; the main thread never moves. Classic Unity-flavor async deadlock.

Anatomy of the Deadlock

Unity registers a custom SynchronizationContext on the main thread. Any await not configured otherwise captures this context and schedules continuations on the main thread.

The sequence:

  1. Main thread calls FetchAsync(), gets a Task.
  2. .Result blocks the main thread waiting for the Task.
  3. Inside FetchAsync, an await finishes and schedules its continuation on the main thread.
  4. The continuation can’t run because the main thread is blocked on Result.
  5. Result can’t complete because the continuation can’t run.
  6. Deadlock.

Fix 1: Make Everything async

async Task Start() {
    var data = await FetchAsync();
    PopulateUI(data);
}

The main thread suspends at await instead of blocking. When the Task completes, the continuation runs on the main thread. No deadlock.

Unity supports async lifecycle methods (Start, Update) via the AsyncTask pattern. async void Start() works; async Task Start() works.

Fix 2: Switch to UniTask

using Cysharp.Threading.Tasks;

async UniTask Start() {
    var data = await FetchAsync();   // returns UniTask<T>
    PopulateUI(data);
}

UniTask uses Unity’s PlayerLoop instead of SynchronizationContext. No deadlock surface. Zero-allocation in many cases. Drop-in for new code; small port for existing Task-based code.

Fix 3: ConfigureAwait(false) Inside Library Code

async Task<Data> FetchAsync() {
    HttpResponseMessage r = await client.GetAsync(url).ConfigureAwait(false);
    return await r.Content.ReadAsAsync<Data>().ConfigureAwait(false);
}

Continuations don’t need the main thread. The library now works whether the caller awaits or blocks. Still risky from Unity callers because callers might touch Unity APIs after .Result, which require main thread.

Fix 4: Task.Run for Background Work

If you really need a synchronous result on the main thread from CPU-bound work:

Data data = Task.Run(() => SyncBlockingWork()).Result;

The work runs on a background thread; the main thread blocks until done. No SynchronizationContext involvement because the background thread doesn’t have UnitySync set. Works for pure C# work; doesn’t work for code that touches Unity APIs (those need main thread).

Diagnosing

If Unity freezes on a specific line, attach a debugger and break on the freeze. The main thread is stuck at Result or Wait. The Task’s continuation is pending. That’s the deadlock.

Verifying

Replace .Result with await. Run; loading screen completes; gameplay continues. Add a Stopwatch around the async section to confirm it returns in reasonable time, not infinite.

“Async all the way or use UniTask. Mixing async with blocking on a single-threaded sync context is a deadlock.”

Forbid .Result and .Wait in your project’s lint rules — saves the inevitable async deadlock that ships otherwise.