Async/Await
Understand asynchronous programming as non-blocking waits—scheduling, suspension points, context, and pitfalls—with language-agnostic models and C# /.NET examples.
Async/await is syntax for writing asynchronous code in a shape that resembles sequential logic. Underneath, the program suspends at await points, frees the thread (or stack) to do other work, and resumes when the awaited operation completes. The concept exists across C#, JavaScript/TypeScript, Python, Rust (async/.await), and others; the runtime details differ, but the mental model is shared.
The problem async solves
I/O (network, disk, timers) takes orders of magnitude longer than CPU instructions. A blocking call holds a thread while nothing useful happens on the CPU. Under load, you exhaust the thread pool and latency collapses.
Asynchronous I/O registers interest with the OS/runtime and continues when data is ready. Async/await lets you express that without nested callbacks.
- Awaitable
An object representing an in-flight operation that can complete later—
Task/ValueTaskin .NET,Promisein JavaScript, coroutine handles in other ecosystems.
Sequential shape, suspended execution
public async Task<OrderDto> GetOrderAsync(OrderId id, CancellationToken ct)
{
var order = await _orders.GetAsync(id, ct); // suspend
var customer = await _customers.GetAsync(order.CustomerId, ct); // suspend
return OrderDto.From(order, customer);
}Each await:
- Checks if the operation already completed (fast path)
- If not, schedules a continuation for when it completes
- Returns control to the caller
The method returns a Task to its caller before the body finishes—hence async methods compose.
Concurrency vs parallelism
| Term | Meaning |
|---|---|
| Asynchronous | Non-blocking waits; may use one thread interleaved |
| Concurrent | Multiple tasks in progress (logically overlapping) |
| Parallel | Multiple threads/cores executing simultaneously |
async does not mean “run on another thread.” CPU-bound work needs explicit parallelisation (Task.Run, parallel loops, etc.). Awaiting I/O is about not blocking, not about using more cores.
Composition
Sequential awaits run one after another—correct when each step depends on the previous.
Concurrent starts overlap independent I/O:
var orderTask = _orders.GetAsync(id, ct);
var inventoryTask = _inventory.GetAsync(sku, ct);
await Task.WhenAll(orderTask, inventoryTask);
var order = await orderTask;
var inventory = await inventoryTask;var a = await GetAAsync(ct);var b = await GetBAsync(ct);return (a, b);var aTask = GetAAsync(ct);var bTask = GetBAsync(ct);await Task.WhenAll(aTask, bTask);return (await aTask, await bTask);Cancellation and timeouts
Pass CancellationToken through async APIs so abandoned requests stop work. Timeouts are cancellation with a deadline:
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
cts.CancelAfter(TimeSpan.FromSeconds(5));
await _client.SendAsync(request, cts.Token);Ignoring cancellation is a common source of wasted work under load—relevant when scaling writes and fan-out.
Context and synchronisation
In classic .NET UI and older ASP.NET, await captured a synchronisation context and resumed on it. In ASP.NET Core, there is typically no such context—continuations run on thread-pool threads. ConfigureAwait(false) matters in libraries targeting contexts that might deadlock; in modern ASP.NET Core app code it is often unnecessary but still appears in reusable libraries.
Deadlock pattern (legacy sync-over-async):
// Dangerous in contexts with a single-threaded sync context
var result = GetDataAsync().Result; // blocks thread that continuation needsPrefer async all the way down. If you must bridge, understand the context—or use patterns carefully at the boundary.
Error handling
Exceptions from awaited tasks surface at the await (or when observing the task). Task.WhenAll aggregates failures. Unobserved faults can be problematic—always await or explicitly handle tasks you start.
Language-agnostic checklist
| Concern | Practice |
|---|---|
| I/O-bound work | Async APIs end-to-end |
| CPU-bound work | Explicit parallelism, not blind async |
| Independence | Start then WhenAll / gather |
| Abandonment | Thread cancellation tokens |
| Libraries | Avoid sync-over-async; document context assumptions |
| Diagnostics | Preserve Activity/trace ids across awaits |
Relation to system design
Async I/O lets a single host handle many concurrent requests—necessary but not sufficient for scale. Durable decoupling still uses messaging (event-driven architecture). Domain modules in a modular monolith expose async ports so adapters can call databases and brokers without blocking the request pipeline. See the .NET section for platform-specific hosting notes as that content grows.