How to use async and await in C#
The async and await keywords in C# enable asynchronous programming. An async method can contain await expressions that suspend execution until an awaited task completes, allowing other work to proceed.
Methods marked with async typically return Task or Task<T>. The await keyword unwraps the result from a task without blocking the calling thread.
Async methods should have "Async" suffix by convention (e.g., GetDataAsync), and you should avoid async void except for event handlers.
Basic Async/Await Usage
using System.Net.Http;
Console.WriteLine("Starting async operation...");
// Call async method with await
string result = await FetchDataAsync();
Console.WriteLine($"Result: {result}");
// Multiple async calls in sequence
await Task.Delay(1000); // Wait 1 second
Console.WriteLine("One second passed");
// Running multiple tasks concurrently
Task<int> task1 = CalculateAsync(5);
Task<int> task2 = CalculateAsync(10);
int[] results = await Task.WhenAll(task1, task2);
Console.WriteLine($"Results: {results[0]}, {results[1]}");
Console.WriteLine("All operations completed!");
static async Task<string> FetchDataAsync()
{
// Simulate async operation
await Task.Delay(500);
return "Data fetched successfully";
}
static async Task<int> CalculateAsync(int value)
{
await Task.Delay(200);
return value * 2;
}Working with HttpClient
static async Task<string> GetWebContentAsync(string url)
{
using HttpClient client = new HttpClient();
string content = await client.GetStringAsync(url);
return content;
}Async with try/catch
Exceptions from awaited tasks propagate normally and can be caught with standard try/catch blocks.
static async Task<string> FetchSafelyAsync(string url)
{
try
{
using HttpClient client = new HttpClient();
client.Timeout = TimeSpan.FromSeconds(10);
return await client.GetStringAsync(url);
}
catch (HttpRequestException ex)
{
Console.WriteLine($"Network error: {ex.Message}");
return string.Empty;
}
catch (TaskCanceledException)
{
Console.WriteLine("Request timed out");
return string.Empty;
}
}
// Task.WhenAll — the first exception is re-thrown; wrap in try/catch as normal
async Task RunMultipleAsync()
{
try
{
await Task.WhenAll(
Task.FromException(new InvalidOperationException("Task 1 failed")),
Task.Delay(100)
);
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"Caught: {ex.Message}"); // Caught: Task 1 failed
}
}async void — The Anti-Pattern
async void methods cannot be awaited, and any exception they throw will crash the application. Only use async void for event handlers.
// BAD — exceptions crash the app, caller cannot await or catch
async void LoadDataBad()
{
await Task.Delay(1000);
throw new Exception("This exception cannot be caught by the caller!");
}
// GOOD — async Task lets callers await and catch exceptions
async Task LoadDataGoodAsync()
{
await Task.Delay(1000);
}
// ACCEPTABLE — async void only in event handlers
button.Click += async (sender, e) =>
{
await LoadDataGoodAsync(); // properly awaited inside the void handler
};ConfigureAwait(false)
By default, await captures the current synchronization context and resumes on it. In library code, use ConfigureAwait(false) to skip this overhead and avoid potential deadlocks.
// Library code — ConfigureAwait(false) continues on a thread pool thread
static async Task<string> LibraryMethodAsync()
{
await Task.Delay(100).ConfigureAwait(false);
// Continues on a thread pool thread, not the original context
return "result";
}
static async Task<byte[]> DownloadAsync(string url)
{
using HttpClient client = new HttpClient();
// Each await in library code should use ConfigureAwait(false)
byte[] data = await client.GetByteArrayAsync(url).ConfigureAwait(false);
return data;
}
// ASP.NET Core and console apps have no synchronization context,
// so ConfigureAwait(false) is optional but not harmful there.Task vs Task<T> vs ValueTask<T>
// Task — async operation with no return value
async Task SendEmailAsync(string to, string body)
{
await Task.Delay(100); // simulate sending
Console.WriteLine($"Email sent to {to}");
}
// Task<T> — async operation that returns a value
async Task<int> CountUsersAsync()
{
await Task.Delay(100); // simulate DB query
return 42;
}
// ValueTask<T> — prefer when the result is often available synchronously.
// Avoids a heap allocation on the fast path compared to Task<T>.
private int _cachedValue = -1;
async ValueTask<int> GetCachedValueAsync()
{
if (_cachedValue != -1)
return _cachedValue; // synchronous — no Task object allocated
await Task.Delay(100); // async path allocates only when needed
_cachedValue = 99;
return _cachedValue;
}Common Deadlock Scenario to Avoid
Calling .Result or .Wait() on an async method from a thread that owns a synchronization context (WinForms, WPF, classic ASP.NET) blocks the thread while its continuation waits for the same thread — a deadlock.
// DEADLOCK in UI / classic ASP.NET — never block on async code
string bad1 = FetchDataAsync().Result; // deadlocks!
FetchDataAsync().Wait(); // deadlocks!
// CORRECT — always await
string good = await FetchDataAsync();
// If you truly must call async code synchronously (avoid when possible),
// offload to the thread pool to escape the synchronization context:
string fallback = Task.Run(() => FetchDataAsync()).GetAwaiter().GetResult();
static async Task<string> FetchDataAsync()
{
await Task.Delay(500);
return "data";
}Take It Further

Struggling with async/await? The definitive recipe book for every concurrency pattern in .NET.
Concurrency in C# Cookbook — 2nd Edition · Stephen Cleary
Get it on Amazon →As an Amazon Associate I earn from qualifying purchases.