Async and Await in C#
Writing non-blocking code with async/await and understanding what Task actually represents.
2 min read
Some operations — network calls, file reads, database queries — take real time and don't need the CPU while they wait. async/await lets your code start such an operation and free up the current thread until it completes, instead of blocking.
A synchronous, blocking example
string DownloadPage(string url)
{
// blocks the calling thread until this finishes
return new HttpClient().GetStringAsync(url).Result;
}Calling .Result on an asynchronous operation forces the current thread to sit idle, waiting. In a web server handling many requests concurrently, that's a thread doing nothing useful while it could be serving another request.
The async/await version
async Task<string> DownloadPageAsync(string url)
{
using var client = new HttpClient();
string content = await client.GetStringAsync(url);
return content;
}await doesn't block the thread — it suspends the method at that point and returns control to the caller, resuming automatically once the awaited operation completes. Under the hood, the compiler transforms this into a state machine, but you get to write it as if it were simple, linear, top-to-bottom code.
Task and Task<T>
Task represents an operation that will finish at some point, without necessarily producing a value. Task<T> is the same thing but with a result of type T once it's done — Task<string> above eventually produces a string.
By convention, async methods are named with an Async suffix (DownloadPageAsync, GetStringAsync) — a signal to callers that they should await it rather than call it like an ordinary synchronous method.
Awaiting multiple operations
Sequential awaits run one after another:
var page1 = await DownloadPageAsync(url1);
var page2 = await DownloadPageAsync(url2); // doesn't start until page1 finishesIf the operations don't depend on each other, run them concurrently instead:
var task1 = DownloadPageAsync(url1);
var task2 = DownloadPageAsync(url2);
await Task.WhenAll(task1, task2); // both run concurrently
Console.WriteLine(task1.Result);
Console.WriteLine(task2.Result);Calling the method without await starts it immediately and returns a Task you can hold onto; Task.WhenAll then waits for every task in the group to finish, rather than waiting for each one in turn.
async all the way
Once a method is async, callers up the chain generally need to be async too and await it — this is often called "async all the way." Mixing in .Result or .Wait() partway up that chain to "escape" back to synchronous code is a common source of deadlocks in real applications, particularly in UI and older ASP.NET applications with a synchronization context. The safe rule: if you're calling something async, await it, and let async propagate outward through your own method signatures.
Test what you just learned
4 quick questions. Get all of them right to unlock the next lesson.
You can take the quiz without an account — logging in just lets your result count toward your progress.