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.

Create a free account to save this page, take notes and track your progress. Sign in

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 async method returns Task (no value), Task<T> (a value) or ValueTask<T>. Name them with an Async suffix.
  • await pauses 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 throw OperationCanceledException.
  • Task.Delay simulates waiting. Task.Run pushes CPU-bound work to the thread pool.

Pitfalls:

  • Never block on a task with .Result or .Wait(). It can deadlock and wastes a thread; await it instead.
  • Avoid async void except 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

Sequential vs concurrent calls, with cancellation
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 .Result or .Wait(). They block the thread and can deadlock; use await.
  • Forgetting to await a task. The method continues immediately and exceptions get lost; the compiler warns about it.
  • Using async void. Return Task unless you are writing an event handler.
  • Awaiting inside a loop when calls are independent. Collect the tasks and await Task.WhenAll instead.
  • Ignoring cancellation tokens. Accept and pass a CancellationToken through public async APIs.

Key points

  • async methods return Task or Task<T>; await pauses without blocking a thread.
  • Start independent operations together and await them with Task.WhenAll.
  • Pass a CancellationToken for timeouts and user cancellation; handle OperationCanceledException.
  • 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.

Was this page helpful?