async and await

Many operations spend most of their time waiting: for a web server to reply, a database query to return, a file to be read. async and await let your code wait without blocking a thread, so a web server can handle thousands of requests and a desktop app stays responsive. The code still reads top to bottom, like ordinary synchronous code.

Tasks

An asynchronous operation is represented by a Task (no result) or a Task<T> (a result of type T) — a promise that the work will complete later. await pauses the current method until the task finishes and gives you its result:

C#
Console.WriteLine("Fetching price...");
decimal price = await GetPriceAsync("ACME");
Console.WriteLine($"Price: {price}");

static async Task<decimal> GetPriceAsync(string symbol)
{
    await Task.Delay(500);         // stands in for a network call
    return symbol.Length * 10.5m;
}
Output
Fetching price...
Price: 42.0

Three things to notice:

  • The method is marked async and returns Task<decimal>, yet its return statement returns a plain decimal — the compiler wraps it in the task.
  • By convention, async method names end in Async.
  • Top-level statements can use await directly; the hidden Main becomes async automatically.

What await really does

While a method is awaiting, its thread isn't stuck — it's released to do other work. When the awaited task completes, the rest of the method continues (possibly on a different thread). Compare await Task.Delay(500), which frees the thread for half a second, with Thread.Sleep(500), which holds it doing nothing. In a server handling many requests, that difference is everything.

Running tasks concurrently

Awaiting each call one after another runs them in sequence. To run independent operations at the same time, start them all first, then await them together with Task.WhenAll:

C#
using System.Diagnostics;

var watch = Stopwatch.StartNew();
string a = await DownloadAsync("a.txt", 300);
string b = await DownloadAsync("b.txt", 300);
string c = await DownloadAsync("c.txt", 300);
Console.WriteLine($"Sequential: {a}, {b}, {c} in about {Rounded(watch)} ms");

watch.Restart();
Task<string> ta = DownloadAsync("a.txt", 300);
Task<string> tb = DownloadAsync("b.txt", 300);
Task<string> tc = DownloadAsync("c.txt", 300);
string[] all = await Task.WhenAll(ta, tb, tc);
Console.WriteLine($"Concurrent: {string.Join(", ", all)} in about {Rounded(watch)} ms");

static async Task<string> DownloadAsync(string name, int ms)
{
    await Task.Delay(ms);
    return name.ToUpper();
}

static long Rounded(Stopwatch w) => (w.ElapsedMilliseconds + 150) / 300 * 300;
Output
Sequential: A.TXT, B.TXT, C.TXT in about 900 ms
Concurrent: A.TXT, B.TXT, C.TXT in about 300 ms

Calling an async method starts it immediately and returns its task; await is where you wait for the result. Task.WhenAll returns the results in the same order as the tasks. (The timings are rounded to the nearest 300 ms so they're stable from run to run.)

WhenAny: first to finish

Task.WhenAny completes as soon as any one task does — useful for timeouts or taking the fastest of several sources:

C#
Task<string> mirror1 = Fetch("mirror-eu", 400);
Task<string> mirror2 = Fetch("mirror-us", 150);
Task<string> winner = await Task.WhenAny(mirror1, mirror2);
Console.WriteLine($"Fastest: {await winner}");

static async Task<string> Fetch(string server, int ms)
{
    await Task.Delay(ms);
    return server;
}
Output
Fastest: mirror-us

Exceptions in async code

An exception thrown inside an async method is stored in its task and re-thrown where the task is awaited, so ordinary try/catch works:

C#
try
{
    await LoadUserAsync(-1);
}
catch (ArgumentException ex)
{
    Console.WriteLine($"Failed: {ex.Message}");
}

static async Task<string> LoadUserAsync(int id)
{
    await Task.Delay(100);
    if (id < 0) throw new ArgumentException("Invalid user id.");
    return $"user{id}";
}
Output
Failed: Invalid user id.

Cancellation

Long-running operations should be cancellable. The caller creates a CancellationTokenSource and passes its Token down; async methods pass it on to the methods they call, and cancelled operations throw OperationCanceledException:

C#
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(350));

try
{
    await ProcessItemsAsync(10, cts.Token);
}
catch (OperationCanceledException)
{
    Console.WriteLine("Cancelled: took too long.");
}

static async Task ProcessItemsAsync(int count, CancellationToken token)
{
    for (int i = 1; i <= count; i++)
    {
        await Task.Delay(100, token);
        Console.WriteLine($"Processed item {i}");
    }
}
Output
Processed item 1
Processed item 2
Processed item 3
Cancelled: took too long.

Library methods that support cancellation take a CancellationToken as their last parameter. In ASP.NET Core you receive one for each request, which is cancelled if the client disconnects.

A real example: HTTP

Network calls are the most common use of async code. HttpClient fetches web resources; create one and reuse it for the life of the program. (This example needs an internet connection, so its output isn't shown.)

C#
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(10) };

string html = await http.GetStringAsync("https://example.com");
Console.WriteLine($"Downloaded {html.Length} characters");

using HttpResponseMessage response = await http.GetAsync("https://example.com/missing");
Console.WriteLine($"Status: {(int)response.StatusCode} {response.StatusCode}");

Async streams

An async iterator returns IAsyncEnumerable<T>, producing values over time; consume it with await foreach. It's ideal for paged APIs and live data feeds:

C#
await foreach (int reading in ReadSensorAsync(4))
{
    Console.WriteLine($"Reading: {reading}");
}

static async IAsyncEnumerable<int> ReadSensorAsync(int count)
{
    for (int i = 0; i < count; i++)
    {
        await Task.Delay(50);       // wait for the next sample
        yield return 20 + i * 3;
    }
}
Output
Reading: 20
Reading: 23
Reading: 26
Reading: 29

Rules of thumb

  • Async all the way: if a method awaits something, make it async and have its callers await it too.
  • Don't block on tasks with .Result or .Wait(). It wastes a thread and can deadlock in some environments. Use await.
  • Avoid async void except for event handlers: exceptions from it can't be caught by the caller. Return Task instead.
  • Async is for waiting, not for computing. It doesn't make CPU-heavy work faster; for that, see parallelism in the next lesson.
  • Use ValueTask<T> only in hot paths that usually complete synchronously; Task<T> is the right default.

Try it yourself

Simulate making breakfast: start the coffee, eggs and toast together and wait for all of them. Try awaiting each one immediately instead and compare the order of the messages.

Try it yourself · Program.cs
Edit the code, copy it into Program.cs and run it with dotnet run Program.cs (.NET 10 or later). How to set up .NET
Output
Starting coffee
Starting eggs
Starting toast
toast done
coffee done
eggs done
Breakfast is ready!