← Back to C# series
✨
Modern C#
Modern C# Β· Prerequisite: delegate

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.

C#.NET 8asyncasynchronous
Duration
⏱ ~1-1.5 hours
Level
πŸ“Š Intermediate
Prerequisite
🎯 Delegate and lambda
OUTCOME
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.

csharp
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 synchronous

2) 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**.

csharp
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 finisher

4) Cancellation β€” `CancellationToken`

Cooperative cancellation. The receiver must **check the token** to actually stop.

csharp
using CancellationTokenSource cts = new();
cts.CancelAfter(2000);
await Task.Delay(5000, cts.Token);   // OperationCanceledException at 2 sec

Most 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.

csharp
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.

csharp
await stream.ReadAsync(buf).ConfigureAwait(false);

Library authors should add it reflexively; app code can ignore it.

Examples

Example 1 β€” `AsyncBasics`: basic flow

csharp
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**

text
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

csharp
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**

text
  [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

csharp
Task<string> winner = await Task.WhenAny(mirrorA, mirrorB, mirrorC);
string result = await winner;

**Output**

text
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

csharp
using CancellationTokenSource cts = new();
cts.CancelAfter(TimeSpan.FromMilliseconds(1500));

try { await LongJobAsync(cts.Token); }
catch (OperationCanceledException) { Console.WriteLine("work was canceled"); }

**Output**

text
  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

csharp
try { await FailingAsync(); }
catch (InvalidOperationException ex) { Console.WriteLine($"caught: {ex.Message}"); }

**Output**

text
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

xml
<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

csharp
// .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

xml
<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

csharp
// 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

xml
<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

csharp
// 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

csharp
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

xml
<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

csharp
// 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

xml
<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

  1. Using `async void` β€” the caller can't catch the exception and the app crashes. Only for event handlers.
  2. Calling `task.Result` / `task.Wait()` β€” a deadlock magnet in UI contexts. Always `await`.
  3. Wrapping simple I/O in `Task.Run(async () => ...)` β€” I/O is already async; no need to hit the thread pool.
  4. Looking at only the first exception from `WhenAll` and ignoring the rest β€” check all to avoid swallowing failures.
  5. 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

text
5 seconds left
4 seconds left
3 seconds left
Canceled

Hints

  • 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)

text
[A] start
[B] start
[C] start
[A] done (700ms)
[C] done (900ms)
[B] done (1200ms)
Sum: 2800
Elapsed: ~1200ms

Hints

  • 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

xml
<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

csharp
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

xml
<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

csharp
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

bash
cd src/AsyncBasics && dotnet run
cd ../WhenAll && dotnet run
cd ../WhenAny && dotnet run
cd ../Cancellation && dotnet run
cd ../AsyncException && dotnet run

Next 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.

Example code / lecture materials

All lecture materials and example code are openly available on GitHub.

View on GitHub β†—