Async and Await in C#: A Practical Guide
Codevity Admin · 10/24/2026
Codevity Admin · 10/24/2026
and let a method wait for slow work, such as a database query or an HTTP call, without holding a thread hostage while it waits. In a web server, that is the difference between serving a handful of requests at once and serving thousands.
Marking a method does not make anything run in parallel. It is the I/O (the network, the disk, the database) that happens without a thread; your code runs on whichever thread resumes it.
Once a method is async, its callers should be async as well. Do not block on a task with or :
Blocking wastes the very threads async was meant to free, and exceptions get wrapped in an .
exists for event handlers only. An exception thrown inside one cannot be caught by the caller and can crash the process, and the caller cannot await it. Return instead.
Awaiting one call after another runs them one at a time. If the calls do not depend on each other, start them together:
Let callers stop work that is no longer needed, for example when a user closes the page. ASP.NET Core supplies a token you can add to an action's parameters:
Pass the token down to every async call that accepts one (, EF Core's , ).
Wrapping an already-async call in only adds overhead. Use it to move genuinely CPU-heavy work off a UI thread, never to make a web request "async".
In ASP.NET Core there is no synchronization context, so makes no practical difference in application code. Library authors still use it so the library behaves well when called from UI apps.
An exception in an async method is stored in the returned task and rethrown when you it, so a normal / works:
If you start a task and never await it, its exceptions disappear silently. Always await what you start, or deliberately hand it to a background service.
asyncawaitasyncTaskTask<T>ValueTask<T>awaitAsyncasync.Result.Wait()AggregateExceptionasync voidasync voidTaskHttpClientToListAsync(ct)Task.Delay(delay, ct)Task.RunTask.RunConfigureAwait(false)awaittrycatchpublic async Task<string> GetTitleAsync(HttpClient http, string url)
{
string html = await http.GetStringAsync(url); // the method pauses here, the thread goes back to the pool
return ExtractTitle(html); // resumes here when the response has arrived
}// Risky: blocks a thread, and can deadlock in apps with a synchronization context (UI apps, older ASP.NET)
var title = GetTitleAsync(http, url).Result;
// Correct
var title = await GetTitleAsync(http, url);var userTask = repo.GetUserAsync(id);
var ordersTask = repo.GetOrdersAsync(id);
await Task.WhenAll(userTask, ordersTask);
var user = await userTask; // already complete, returns immediately
var orders = await ordersTask;[HttpGet("report")]
public async Task<IActionResult> Report(CancellationToken ct)
{
var data = await _service.BuildReportAsync(ct);
return Ok(data);
}try
{
await SaveAsync(order, ct);
}
catch (DbUpdateException ex)
{
_logger.LogError(ex, "Could not save order {OrderId}", order.Id);
throw; // keep the original stack trace
}