async and await
Write asynchronous code that reads like synchronous code — and understand what await really does.
13 min read
You will learn
- Write async methods returning Task and Task<T>
- Explain what happens at an await
- Run independent awaits concurrently
- Handle exceptions in async code
Asynchronous code that reads top to bottom
Before async/await, asynchronous code meant callbacks nested inside callbacks. C#'s async and await let you write it like ordinary sequential code:
async Task<decimal> GetOrderTotalAsync(int orderId)
{
Order order = await db.Orders.FindAsync(orderId); // wait for the database
Customer customer = await crm.GetCustomerAsync(order.CustomerId); // wait for another service
return order.Total * (1 - customer.Discount);
}asyncmarks a method that containsawait.awaitwaits for a task to complete without blocking the thread, then gives you its result.- The method returns
Task<T>(orTaskwhen there's no value) — its caller can await it in turn.
By convention, async method names end in Async.
What await really does
At each await, if the task isn't finished yet:
- The method pauses and returns an incomplete
Taskto its caller immediately. - The thread is released to do other work (serve another request, keep the UI responsive).
- When the awaited operation completes, the method resumes right after the
await— possibly on a different thread.
The compiler rewrites your method into a state machine that remembers where it left off and its local variables. If the task is already complete, await simply continues without pausing.
Return types
| Return type | Use for |
|---|---|
Task | Async work with no result |
Task<T> | Async work producing a T |
ValueTask<T> | Hot paths that often complete synchronously (advanced) |
void | Only event handlers (see next lesson) |
IAsyncEnumerable<T> | Async streams consumed with await foreach |
Top-level programs can use await directly — the editors in this course do.
Sequential vs concurrent
Each await waits before moving on. When operations don't depend on each other, start them all first, then await:
// Sequential: ~2 seconds
var weather = await GetWeatherAsync();
var news = await GetNewsAsync();
// Concurrent: ~1 second
Task<Weather> weatherTask = GetWeatherAsync(); // starts now
Task<News> newsTask = GetNewsAsync(); // starts now
await Task.WhenAll(weatherTask, newsTask);
var (weather2, news2) = (weatherTask.Result, newsTask.Result);
// Many items: project to tasks, then WhenAll
string[] pages = await Task.WhenAll(urls.Select(url => http.GetStringAsync(url)));Exceptions
An exception inside an async method is stored in the returned task and rethrown at the await — so ordinary try/catch works:
try
{
var data = await http.GetStringAsync("https://example.invalid");
}
catch (HttpRequestException ex)
{
Console.WriteLine($"Request failed: {ex.Message}");
}With Task.WhenAll, awaiting rethrows the first exception; all of them are available in the task's Exception.InnerExceptions.
async all the way
Once a method awaits, its callers should await it too — up to the top (an ASP.NET Core endpoint, Main, an event handler). Mixing in blocking calls (.Result, .Wait()) defeats the purpose and can deadlock. You'll see these traps in detail next.
Try it
C# runtime not loaded
Key takeaways
asyncmethods returnTask/Task<T>;awaitwaits without blocking the thread.- At an incomplete
await, the method returns to its caller and resumes later via a compiler-generated state machine. - Start independent operations first, then
await Task.WhenAll(...)to run them concurrently. - Exceptions surface at the
await— use normaltry/catch.
Exercise
FetchAsync simulates a slow web request (200 ms) — don't change it. Implement TotalLengthAsync, which fetches each URL and adds up the lengths of the responses, and FetchAllAsync, which fetches all URLs CONCURRENTLY and returns the responses in the original order.
C# runtime not loaded
Hint 1
TotalLengthAsync: loop over the URLs; inside the loop, string page = await FetchAsync(url); total += page.Length;
Hint 2
FetchAllAsync: first START every request without awaiting — var tasks = urls.Select(FetchAsync); — then return await Task.WhenAll(tasks);
Show solution
var pages = await FetchAllAsync(new[] { "home", "about" });
Console.WriteLine(string.Join(" | ", pages));
async Task<string> FetchAsync(string url)
{
await Task.Delay(200); // a slow network call
return $"<{url}>";
}
async Task<int> TotalLengthAsync(string[] urls)
{
int total = 0;
foreach (var url in urls)
{
string page = await FetchAsync(url);
total += page.Length;
}
return total;
}
async Task<string[]> FetchAllAsync(string[] urls)
{
var tasks = urls.Select(FetchAsync);
return await Task.WhenAll(tasks);
}Quiz
4 questions · 3 correct to pass1.What happens when execution reaches `await SomethingAsync()` and the task isn't finished yet?
2.What should an async method that produces no value return?
3.`var a = await GetAAsync(); var b = await GetBAsync();` — the two calls are independent. How can you make them run concurrently?
4.How do you catch an exception thrown inside an awaited async method?
To complete this lesson
- ○ Solve the exercise
- ○ Pass the quiz (3/4 correct)