C# Fundamentals: From Your First Program to Async and JSON · Modern C# in Practice: Generics, Async, Errors and JSON
async/await and Tasks
Write asynchronous code that doesn't block threads, run operations concurrently with Task.WhenAll, support cancellation and timeouts, and avoid the classic async pitfalls.
What you'll build
A report generator that calls three simulated slow services. First sequentially, then concurrently with Task.WhenAll, then with a cancellation timeout, so you can see how much time concurrency saves.
The idea
I/O (network calls, files, databases) spends most of its time waiting. async/await lets a thread do other work while it waits, instead of blocking.
- An
asyncmethod returnsTask(no value),Task<T>(a value) orValueTask<T>. Name them with anAsyncsuffix. awaitpauses the method until the task completes, then continues, without blocking the thread.- Start several operations first, then
await Task.WhenAll(...)to run them concurrently. Awaiting them one by one runs them in sequence. CancellationToken: pass a token through your async methods so callers can cancel (user clicks cancel, request times out).CancellationTokenSource(TimeSpan)cancels after a delay; methods throwOperationCanceledException.Task.Delaysimulates waiting.Task.Runpushes CPU-bound work to the thread pool.
Pitfalls:
- Never block on a task with
.Resultor.Wait(). It can deadlock and wastes a thread;awaitit instead. - Avoid
async voidexcept for event handlers, because its exceptions can't be caught by the caller. - Async all the way: if a method awaits, its callers should await too.
The code
using System.Diagnostics;
// Sequential: total time is the sum of the delays
var watch = Stopwatch.StartNew();
var a = await FetchAsync("users", 120);
var b = await FetchAsync("orders", 120);
var c = await FetchAsync("stock", 120);
var sequential = watch.ElapsedMilliseconds;
Console.WriteLine($"sequential results: {a}, {b}, {c}");
// Concurrent: start all three, then await them together
watch.Restart();
var results = await Task.WhenAll(
FetchAsync("users", 120),
FetchAsync("orders", 120),
FetchAsync("stock", 120));
var concurrent = watch.ElapsedMilliseconds;
Console.WriteLine($"concurrent results: {string.Join(", ", results)}");
Console.WriteLine($"concurrent was faster: {concurrent < sequential}");
// A timeout through cancellation
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(50));
try
{
await FetchAsync("slow report", 2000, cts.Token);
Console.WriteLine("finished");
}
catch (OperationCanceledException)
{
Console.WriteLine("slow report cancelled after the timeout");
}
// The first of several tasks to finish
var winner = await Task.WhenAny(FetchAsync("primary", 200), FetchAsync("replica", 20));
Console.WriteLine($"first to finish: {await winner}");
// A pitfall: each await must be awaited. This loop starts tasks one at a time, in order.
var squares = new List<int>();
foreach (var n in new[] { 1, 2, 3 })
squares.Add(await SquareAsync(n));
Console.WriteLine("squares: " + string.Join(", ", squares));
static async Task<string> FetchAsync(string what, int delayMs, CancellationToken token = default)
{
await Task.Delay(delayMs, token); // pretend this is a network call
return what;
}
static async Task<int> SquareAsync(int n)
{
await Task.Delay(1);
return n * n;
}
Output
sequential results: users, orders, stock concurrent results: users, orders, stock concurrent was faster: True slow report cancelled after the timeout first to finish: replica squares: 1, 4, 9
Run this on your computer — see “Get Started” for setup.
Timing varies slightly between machines, so the program prints only whether the concurrent run was faster. With three 120 ms calls, sequential takes about 360 ms and concurrent about 120 ms.
Common mistakes
- Using
.Resultor.Wait(). They block the thread and can deadlock; useawait. - Forgetting to await a task. The method continues immediately and exceptions get lost; the compiler warns about it.
- Using
async void. ReturnTaskunless you are writing an event handler. - Awaiting inside a loop when calls are independent. Collect the tasks and
await Task.WhenAllinstead. - Ignoring cancellation tokens. Accept and pass a
CancellationTokenthrough public async APIs.
Key points
asyncmethods returnTaskorTask<T>;awaitpauses without blocking a thread.- Start independent operations together and await them with
Task.WhenAll. - Pass a
CancellationTokenfor timeouts and user cancellation; handleOperationCanceledException. - Don't block on tasks and avoid
async void.
Further reading: Asynchronous programming.
Practice questions
Why is `Task.WhenAll(a, b, c)` faster than awaiting a, b and c one after another?
Answer
The tasks are all started before waiting, so their waits overlap and the total time is roughly the longest one. Awaiting each in turn starts the next only after the previous finishes, so the times add up.
What is wrong with calling `.Result` on a task in an async application?
Answer
It blocks the calling thread until the task finishes, which wastes a thread and can deadlock when the task needs to resume on that same thread. Use await so the thread is released while waiting.
Open in the app to mark lessons complete, take notes and track your progress.