How does async/await work in C#?

C# 2 min read
Short answer

Mark a method async and return Task or Task<T>, then await operations that would otherwise block. Await frees the thread instead of holding it while waiting.

csharp
async Task<string> LoadAsync(string url)
{
    using var client = new HttpClient();
    var response = await client.GetAsync(url);
    response.EnsureSuccessStatusCode();
    return await response.Content.ReadAsStringAsync();
}

#Never block on async code

csharp
var result = LoadAsync(url).Result;        // can deadlock
var result = LoadAsync(url).GetAwaiter().GetResult();   // same problem
var result = await LoadAsync(url);         // correct

In a UI or classic ASP.NET context, .Result blocks the thread that the continuation needs in order to finish. Both sides wait forever.

Async all the way up. If a method awaits, its callers should await too.

#Return Task, not void

csharp
async void Bad() { }        // exceptions cannot be caught — crashes the process
async Task Good() { }

The single exception is an event handler, which must return void because the delegate signature demands it. Wrap its whole body in try/catch.

#Run independent work in parallel

csharp
// Sequential — 600ms
var a = await GetA();
var b = await GetB();

// Concurrent — 200ms
var taskA = GetA();
var taskB = GetB();
await Task.WhenAll(taskA, taskB);
var a = taskA.Result;   // safe here — already completed

#Cancellation

csharp
async Task WorkAsync(CancellationToken token)
{
    await Task.Delay(1000, token);
    token.ThrowIfCancellationRequested();
}

Accept a CancellationToken in any long-running async method and pass it down. Callers can then abandon work that is no longer needed.

#ConfigureAwait(false) in libraries

csharp
await client.GetAsync(url).ConfigureAwait(false);

In library code, this says the continuation does not need the original synchronization context. It avoids a class of deadlocks and is slightly faster. Application code generally does not need it.