19. async / await
async/await are the core keywords for non-blocking I/O. Cover Task, Task<T>, Task.WhenAll, exception handling, and cancellation in one lecture.
What you'll learn
- 1Understand the relationship between `Task`/`Task<T>` and `async`/`await`
- 2Wait in parallel with `Task.WhenAll`, take the first finisher with `Task.WhenAny`
- 3Cancel async work safely with `CancellationToken`
- 4Know that exceptions in an async method are thrown at the `await`
- 5Get a high-level sense of `ConfigureAwait(false)`
Overview
**Asynchronous programming** is what stops your code from holding a thread while waiting on I/O. C#'s `async`/`await` lets you write async like synchronous code, without callback hell. This lecture walks through .NET's `Task` model and the core patterns.
Core Concepts
1) `Task` is "a future result"
`Task` is work in progress; `Task<T>` is work that will return `T` when done. `await` waits for the result and unpacks it.
async Task<int> GetAsync()
{
await Task.Delay(1000); // releases the thread for 1 second
return 42;
}
int n = await GetAsync(); // looks asynchronous but reads as synchronous2) Signature of an `async` method
- Return type is `Task`, `Task<T>`, or `ValueTask`/`ValueTask<T>`
- `async void` only for event handlers β **never use it elsewhere** (exceptions can't be caught)
- By convention, suffix the method name with `Async` (`SaveAsync`, `FetchAsync`)
3) Parallel wait β `Task.WhenAll` / `Task.WhenAny`
Sequentially `await`-ing waits one after another; gathering tasks and `await`-ing once runs them **concurrently**.
Task<int> a = FetchAsync("A");
Task<int> b = FetchAsync("B");
int[] all = await Task.WhenAll(a, b); // until all finish
Task<int> first = await Task.WhenAny(a, b); // first finisher4) Cancellation β `CancellationToken`
Cooperative cancellation. The receiver must **check the token** to actually stop.
using CancellationTokenSource cts = new();
cts.CancelAfter(2000);
await Task.Delay(5000, cts.Token); // OperationCanceledException at 2 secMost BCL async methods (`Task.Delay`, `HttpClient.GetAsync`, `Stream.ReadAsync`...) take a `CancellationToken` parameter.
5) Exceptions are rethrown at `await`
An exception inside an async method is stored in the `Task` and **rethrown at the `await` site**. So your `try`/`catch` can live at the caller.
try { await DoAsync(); }
catch (InvalidOperationException ex) { /* ... */ }In `Task.WhenAll`, only the first exception is thrown β the rest are collected in `Task.Exception` (`AggregateException`).
6) `ConfigureAwait(false)` β for library code only
Used in **environments with a UI/request context** (WinForms, WPF, ASP.NET Framework) to avoid the cost of resuming on the same context after `await`. Largely irrelevant in console apps or ASP.NET Core.
await stream.ReadAsync(buf).ConfigureAwait(false);Library authors should add it reflexively; app code can ignore it.
Examples
Example 1 β `AsyncBasics`: basic flow
Console.WriteLine("starting work");
int result = await DoWorkAsync();
Console.WriteLine($"result: {result}");
static async Task<int> DoWorkAsync()
{
await Task.Delay(1000);
await Task.Delay(1000);
return 42;
}**Output**
starting work
...wait 1s
...wait 1s more
result: 42
work finished**Note:** During `await Task.Delay` the thread is free to do other work β the key reason the UI doesn't freeze.
Example 2 β `WhenAll`: start concurrently, wait together
Task<int> t1 = FetchAsync("A", 1000);
Task<int> t2 = FetchAsync("B", 1500);
Task<int> t3 = FetchAsync("C", 800);
int[] results = await Task.WhenAll(t1, t2, t3);**Output**
[A] start (1000ms)
[B] start (1500ms)
[C] start (800ms)
[C] done
[A] done
[B] done
Sum of results: 3300
Total: 1500 ms (would have been ~3300ms sequentially)**Note:** Takes as long as the slowest task. Best pattern for batching multiple I/O calls.
Example 3 β `WhenAny`: take the fastest response
Task<string> winner = await Task.WhenAny(mirrorA, mirrorB, mirrorC);
string result = await winner;**Output**
First to respond: Tokyo(800ms)**Note:** `WhenAny` returns the first-finishing `Task` itself, so you need to `await` it **one more time** to get the value.
Example 4 β `Cancellation`: cancel from outside
using CancellationTokenSource cts = new();
cts.CancelAfter(TimeSpan.FromMilliseconds(1500));
try { await LongJobAsync(cts.Token); }
catch (OperationCanceledException) { Console.WriteLine("work was canceled"); }**Output**
step 1 done
step 2 done
work was canceled**Note:** Inside the method, pass the token with `Task.Delay(ms, token)`. Ignoring the token means it never stops.
Example 5 β `AsyncException`: exception flow
try { await FailingAsync(); }
catch (InvalidOperationException ex) { Console.WriteLine($"caught: {ex.Message}"); }**Output**
caught: intentional failure
caught in WhenAll: intentional failure
t1 status: Faulted, t2 status: Faulted**Note:** An exception thrown inside an async method is wrapped in the Task and rethrown when `await`-ed. Catch with normal `try`/`catch`.
Full example code (src/)
src/AsyncBasics/AsyncBasics.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Modern19</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
src/AsyncBasics/Program.cs
// .NET 8 top-level statements support await directly.
Console.WriteLine("starting work");
int result = await DoWorkAsync();
Console.WriteLine($"result: {result}");
Console.WriteLine("work finished");
// async methods return Task / Task<T>.
// At an await, control returns to the caller and resumes when the task completes.
static async Task<int> DoWorkAsync()
{
Console.WriteLine(" ...wait 1s");
await Task.Delay(1000);
Console.WriteLine(" ...wait 1s more");
await Task.Delay(1000);
return 42;
}
src/AsyncException/AsyncException.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Modern19</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
src/AsyncException/Program.cs
// Exceptions thrown in an async method are "boxed" into the Task and re-thrown at await.
try
{
await FailingAsync();
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"caught: {ex.Message}");
}
// With Task.WhenAll only the first exception is thrown; the rest live in Task.Exception.
Task t1 = FailingAsync();
Task t2 = FailingAsync();
try
{
await Task.WhenAll(t1, t2);
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"caught in WhenAll: {ex.Message}");
Console.WriteLine($" t1 status: {t1.Status}, t2 status: {t2.Status}");
}
static async Task FailingAsync()
{
await Task.Delay(200);
throw new InvalidOperationException("intentional failure");
}
src/Cancellation/Cancellation.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Modern19</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
src/Cancellation/Program.cs
// CancellationToken signals async work to stop from outside.
using CancellationTokenSource cts = new();
// Send the cancel signal after 1.5 seconds.
cts.CancelAfter(TimeSpan.FromMilliseconds(1500));
try
{
await LongJobAsync(cts.Token);
Console.WriteLine("normal completion");
}
catch (OperationCanceledException)
{
Console.WriteLine("work was canceled");
}
// Either check the token at each step or pass it to Task.Delay for auto-cancel.
static async Task LongJobAsync(CancellationToken token)
{
for (int i = 1; i <= 5; i++)
{
await Task.Delay(500, token); // throws OperationCanceledException on cancel
Console.WriteLine($" step {i} done");
}
}
src/WhenAll/Program.cs
using System.Diagnostics;
// Start 3 tasks at once and wait for all to finish.
Stopwatch sw = Stopwatch.StartNew();
Task<int> t1 = FetchAsync("A", 1000);
Task<int> t2 = FetchAsync("B", 1500);
Task<int> t3 = FetchAsync("C", 800);
int[] results = await Task.WhenAll(t1, t2, t3);
sw.Stop();
Console.WriteLine($"Sum of results: {results.Sum()}");
Console.WriteLine($"Total: {sw.ElapsedMilliseconds} ms (would have been ~3300ms sequentially)");
// Simulate a network call asynchronously β in reality you'd await HttpClient etc.
static async Task<int> FetchAsync(string name, int delayMs)
{
Console.WriteLine($" [{name}] start ({delayMs}ms)");
await Task.Delay(delayMs);
Console.WriteLine($" [{name}] done");
return delayMs;
}
src/WhenAll/WhenAll.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Modern19</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
src/WhenAny/Program.cs
// Take whichever task finishes first β "which server replies first?" pattern.
Task<string> mirrorA = MirrorAsync("Seoul", 1500);
Task<string> mirrorB = MirrorAsync("Tokyo", 800);
Task<string> mirrorC = MirrorAsync("Singapore", 1200);
Task<string> winner = await Task.WhenAny(mirrorA, mirrorB, mirrorC);
string result = await winner; // await once more to extract the value.
Console.WriteLine($"First to respond: {result}");
// Let the others finish in the background, or cancel (see next example).
static async Task<string> MirrorAsync(string region, int delayMs)
{
await Task.Delay(delayMs);
return $"{region}({delayMs}ms)";
}
src/WhenAny/WhenAny.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Modern19</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
Common Mistakes
- Using `async void` β the caller can't catch the exception and the app crashes. Only for event handlers.
- Calling `task.Result` / `task.Wait()` β a deadlock magnet in UI contexts. Always `await`.
- Wrapping simple I/O in `Task.Run(async () => ...)` β I/O is already async; no need to hit the thread pool.
- Looking at only the first exception from `WhenAll` and ignoring the rest β check all to avoid swallowing failures.
- Receiving a `CancellationToken` and not passing it to the next call β cancellation doesn't propagate.
Summary
- An `async` method returns `Task`/`Task<T>`; the caller `await`s for the result
- Bundle tasks with `Task.WhenAll`; pick the fastest with `Task.WhenAny`
- External cancellation flows via `CancellationToken` β propagate it through the chain
- Exceptions throw at `await`, so use `try`/`catch` as usual
- Console/server apps rarely need `ConfigureAwait`; UI libraries should care
Practice
**Practice - 19. async / await**
Problem 1 β Cancellable countdown
- Project folder: `Homework01/`
- Key concepts: `async Task`, `Task.Delay`, `CancellationTokenSource`
Requirements
- Write `CountdownAsync(int seconds, CancellationToken token)`.
- Print remaining seconds every 1 second ("5 seconds left", "4 seconds left", ...).
- When it reaches 0, print "Liftoff!".
- In Main, create a `CancellationTokenSource` and signal cancellation after 2.5 seconds so the countdown stops midway.
- On cancellation print "Canceled".
Expected output
5 seconds left
4 seconds left
3 seconds left
CanceledHints
- Pass the token with `await Task.Delay(1000, token);` β `OperationCanceledException` is thrown automatically.
- `cts.CancelAfter(2500);` to cancel after a fixed time.
- Order: `Console.WriteLine($"{i} seconds left");` β `await Task.Delay(1000, token);`.
Problem 2 β Three tasks in parallel
- Project folder: `Homework02/`
- Key concepts: `Task.WhenAll`, `async Task<T>`
Requirements
- Write 3 async tasks `LoadAsync(string name, int delayMs)` β wait then return `delayMs`.
- Start 3 concurrently in Main (e.g. 700ms / 1200ms / 900ms).
- Use `Task.WhenAll` to wait for all and print the sum of returned values.
- Also measure total elapsed time with `Stopwatch` (should match the longest task).
Expected output (sample numbers)
[A] start
[B] start
[C] start
[A] done (700ms)
[C] done (900ms)
[B] done (1200ms)
Sum: 2800
Elapsed: ~1200msHints
- Use `System.Diagnostics.Stopwatch`.
- `int[] results = await Task.WhenAll(t1, t2, t3);` β `results.Sum()`.
- Print order follows completion order (shortest first).
Check your answer
Try it yourself, then compare against the [`answer/`](./answer/) folder.
Answer (answer/)
homework/answer/Homework01/Homework01.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Modern19</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
homework/answer/Homework01/Program.cs
using CancellationTokenSource cts = new();
cts.CancelAfter(TimeSpan.FromMilliseconds(2500)); // auto-cancel after 2.5s
try
{
await CountdownAsync(5, cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("Canceled");
}
static async Task CountdownAsync(int seconds, CancellationToken token)
{
for (int i = seconds; i > 0; i--)
{
Console.WriteLine($"{i} seconds left");
await Task.Delay(1000, token); // pass token β throws when canceled
}
Console.WriteLine("Liftoff!");
}
homework/answer/Homework02/Homework02.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Modern19</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
homework/answer/Homework02/Program.cs
using System.Diagnostics;
Stopwatch sw = Stopwatch.StartNew();
// Start concurrently β takes only as long as the longest task (1200ms).
Task<int> t1 = LoadAsync("A", 700);
Task<int> t2 = LoadAsync("B", 1200);
Task<int> t3 = LoadAsync("C", 900);
int[] results = await Task.WhenAll(t1, t2, t3);
sw.Stop();
Console.WriteLine($"Sum: {results.Sum()}");
Console.WriteLine($"Elapsed: ~{sw.ElapsedMilliseconds}ms");
static async Task<int> LoadAsync(string name, int delayMs)
{
Console.WriteLine($"[{name}] start");
await Task.Delay(delayMs);
Console.WriteLine($"[{name}] done ({delayMs}ms)");
return delayMs;
}
Try It Yourself
cd src/AsyncBasics && dotnet run
cd ../WhenAll && dotnet run
cd ../WhenAny && dotnet run
cd ../Cancellation && dotnet run
cd ../AsyncException && dotnet runNext Lecture
[20_Nullable_Reference_Types](../20_Nullable_%EC%B0%B8%EC%A1%B0_%ED%83%80%EC%9E%85/) β Catch `null` issues at compile time.
All lecture materials and example code are openly available on GitHub.
View on GitHub β