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.

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}");
}

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;
}

ConfigureAwait(false) in Library Code

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

public 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";
}

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

At a glance

Purpose
Applications on the .NET platform
File extension
.cs
Runs in
.NET runtime
Usually used with
.NET SDK and libraries

Specifications & further reading

Related C# documentation

async and await
The async and await keywords let you write asynchronous code that reads almost like ordinary sequential code. Marking a method async allows it to use await, which pauses execution at that point until the awaited operation completes, without blocking the calling thread - freeing it up to do other work in the meantime. This is essential for keeping applications responsive during I/O-bound work like network calls, file access, or database queries.
Task & Task<T>
Task represents an asynchronous operation that may still be running, and Task<T> represents one that will eventually produce a value of type T. Tasks are the building block that async/await is built on top of - an async method actually returns a Task (or Task<T>) that the caller can await, check the status of, or attach continuations to.
Task.WhenAll & Task.WhenAny
Task.WhenAll() lets you run multiple independent asynchronous operations concurrently and await all of them together, completing once every task finishes - much faster than awaiting them one at a time. Task.WhenAny() completes as soon as the first of several tasks finishes, useful for timeout patterns or taking whichever result arrives first.
CancellationToken
CancellationToken provides a standard, cooperative way to signal that an asynchronous operation should stop before it completes naturally. A CancellationTokenSource creates the token and controls when cancellation is requested; the token itself is passed into async methods, which check it periodically (or pass it to another cancellable operation) and can throw an OperationCanceledException to unwind cleanly.