Codectionary / Developer documentation / C#

Async Pitfalls: void vs Task, ConfigureAwait

A few recurring mistakes trip up many C# developers new to async: using async void instead of async Task (which makes exceptions impossible to catch normally), and blocking on async code with .Result or .Wait() (which can cause a deadlock in contexts with a synchronization context, like older ASP.NET or WPF apps). Understanding these pitfalls up front avoids some of the most common async-related bugs.

Read the syntax

Prefer async Task or async Task<T> for awaitable operations. async void is intended mainly for event handlers because callers cannot await it or reliably observe its exceptions.

Avoid blocking on .Result or .Wait in code with a synchronization context; use await through the call chain. ConfigureAwait(false) affects continuation-context capture, not whether work is asynchronous.

Syntax

async Task MethodName() { } // not async void

Examples

async void vs async Task

async void methods can't be awaited, and exceptions inside them can crash the whole application unpredictably.

// Avoid: exceptions here cannot be caught by the caller
async void RiskyOperation()
{
    throw new InvalidOperationException("This will crash unpredictably");
}

// Prefer: exceptions surface normally when awaited
async Task SaferOperationAsync()
{
    throw new InvalidOperationException("This can be caught properly");
}

try
{
    await SaferOperationAsync();
}
catch (InvalidOperationException e)
{
    Console.WriteLine($"Caught: {e.Message}");
}
  1. async void methods can't be awaited, and exceptions inside them can crash the whole application unpredictably.
  2. Prefer async Task or async Task<T> for awaitable operations. async void is intended mainly for event handlers because callers cannot await it or reliably observe its exceptions.
  3. RiskyOperation is declared but never called. Awaiting SaferOperationAsync propagates its exception to the surrounding catch, which prints the message.

The Deadlock Risk of Blocking on Async Code

Calling .Result or .Wait() synchronously on an async method can deadlock in certain application contexts.

// Risky in UI/ASP.NET (classic) contexts - can deadlock
// int result = SomeAsyncMethod().Result;

// Safe: await it properly, all the way up the call chain
async Task<int> GetResultSafelyAsync()
{
    return await SomeAsyncMethod();
}

async Task<int> SomeAsyncMethod()
{
    await Task.Delay(100);
    return 42;
}
  1. Calling .Result or .Wait() synchronously on an async method can deadlock in certain application contexts.
  2. Prefer async Task or async Task<T> for awaitable operations. async void is intended mainly for event handlers because callers cannot await it or reliably observe its exceptions.
  3. The blocking call is commented out. This snippet only declares GetResultSafelyAsync and SomeAsyncMethod; a caller must await GetResultSafelyAsync to receive 42.

ConfigureAwait(false) in Library Code

In library code, ConfigureAwait(false) avoids capturing the original context, which can improve performance and avoid deadlocks.

async Task<string> LoadDataAsync()
{
    // In library code (not top-level app code), this avoids resuming
    // on the original context unnecessarily
    await Task.Delay(500).ConfigureAwait(false);
    return "data";
}

Console.WriteLine(await LoadDataAsync());
  1. In library code, ConfigureAwait(false) avoids capturing the original context, which can improve performance and avoid deadlocks.
  2. Prefer async Task or async Task<T> for awaitable operations. async void is intended mainly for event handlers because callers cannot await it or reliably observe its exceptions.
  3. LoadDataAsync awaits a delay without requesting return to a captured synchronization context, then returns data. The top-level caller awaits it and prints data.

Best practices

  • Avoid async void entirely except for top-level UI event handlers, which are required to return void by their delegate signature
  • Never call .Result or .Wait() on a Task from synchronous code that has a synchronization context - await all the way up the call chain instead
  • Use ConfigureAwait(false) in reusable library code that does not need to resume on a specific UI or request context, but skip it in top-level application/UI code where the context often matters
  • Wrap awaited calls in try/catch to handle exceptions properly - unlike async void, an awaited async Task lets exceptions propagate normally to the caller

Runtime & versions

.NET runtime. Feature availability depends on the language and runtime version used by your project; consult the official reference below.

C# language reference

Specifications & further reading