Why asynchronous code

Most code is synchronous: each line finishes before the next one starts. That is fine for calculations, but some operations spend almost all their time waiting: an HTTP request to a meter data service, a database query, reading a large file. In this synchronous version, Thread.Sleep stands in for a slow network call:

Console.WriteLine("1. Request meter reading");
int kwh = ReadMeter("KE-1042");
Console.WriteLine("3. Carry on with other work");
Console.WriteLine($"4. Reading: {kwh} kWh");

static int ReadMeter(string meterId)
{
    Thread.Sleep(100);   // the thread is blocked: it can do nothing else
    Console.WriteLine($"2. Meter {meterId} answered");
    return 412;
}
1. Request meter reading
2. Meter KE-1042 answered
3. Carry on with other work
4. Reading: 412 kWh

While Thread.Sleep runs, the thread is blocked: it exists, it uses memory, and it does nothing. In a desktop app that thread is the one that draws the window, so the window freezes. In a web server each blocked thread is one fewer thread for other requests, so under load the server runs out of threads long before it runs out of processor time.

Asynchronous programming solves this. The code starts the slow operation, hands the waiting to the operating system, and releases the thread. When the answer arrives, the rest of the method continues. Here is the same program written asynchronously:

Console.WriteLine("1. Request meter reading");
Task<int> reading = ReadMeterAsync("KE-1042");
Console.WriteLine("2. Carry on with other work");
int kwh = await reading;
Console.WriteLine($"4. Reading arrived: {kwh} kWh");

static async Task<int> ReadMeterAsync(string meterId)
{
    await Task.Delay(100);   // stands in for a slow network call
    Console.WriteLine($"3. Meter {meterId} answered");
    return 412;
}
1. Request meter reading
2. Carry on with other work
3. Meter KE-1042 answered
4. Reading arrived: 412 kWh

This time line 2 prints before the meter answers. ReadMeterAsync returns a task straight away, the main program carries on, and await reading is where it finally needs the number. Task.Delay is the asynchronous counterpart of Thread.Sleep: it completes a task after the given time without occupying a thread.

Asynchronous code helps with I/O-bound work (input/output: waiting for a network, disk or database). It does not make CPU-bound work, such as a long calculation, any faster; that work needs a processor core, and the section on blocking shows how to move it off the current thread.

Tasks: Task and Task<T>

A task is an object of type Task or Task<T> (in the namespace System.Threading.Tasks) that represents an operation which may still be running. A Task only tells you when the work is done and whether it succeeded; a Task<T> also carries a result of type T, so a Task<int> promises an int. Every task has a Status, and it completes exactly once: successfully (RanToCompletion), with an exception (Faulted), or cancelled (Canceled).

You rarely create tasks by hand; methods such as HttpClient.GetStringAsync, File.ReadAllTextAsync and Task.Delay return them. When a result is already known, for example from a cache, Task.FromResult wraps it in a completed task, and Task.CompletedTask is a finished Task without a result:

Task<int> cached = Task.FromResult(412);
Console.WriteLine($"{cached.Status}: {cached.Result} kWh");
Console.WriteLine(Task.CompletedTask.Status);

Task<int> slow = Task.Delay(100).ContinueWith(_ => 398);
Console.WriteLine($"Right after starting: {slow.IsCompleted}");
Console.WriteLine($"After awaiting: {await slow} kWh, {slow.Status}");
RanToCompletion: 412 kWh
RanToCompletion
Right after starting: False
After awaiting: 398 kWh, RanToCompletion

By convention the names of methods that return a task end in Async: ReadMeterAsync, SaveChangesAsync. The .Result property used on the first line is safe only because that task is already complete; on a running task it blocks, which is the subject of a later section.

async methods and await

An async method is a method marked with the async modifier. Its return type is normally Task (no result) or Task<T> (a result of type T), and inside it you may use await. When you return 412; from an async Task<int> method, the compiler puts the 412 into the task for you.

The await operator takes a task and suspends the surrounding async method until that task is complete. Then it unwraps the outcome: it gives you the result of a Task<T>, or rethrows the exception the task failed with. While the method is suspended, its thread returns to other work. The compiler achieves this by rewriting the method into a small state machine that remembers where to continue; you write straight-line code and the compiler does the bookkeeping.

Three details explain most of the behavior you will see:

  • An async method runs synchronously, on the caller's thread, up to its first await of an unfinished task. Only then does it return the task to its caller.
  • Awaiting a task that is already complete does not suspend anything; the method simply continues.
  • In a program with top-level statements you may use await directly, as these examples do; the compiler then generates an async Task Main for you (see Your First Program).

Forgetting await

A missing await is the most common async bug. In JavaScript it gives you a promise where you expected a value and nobody complains. C#'s static types catch most cases at compile time, because a Task<int> is not an int:

int temperature = GetTemperatureAsync();
Console.WriteLine(temperature);

static async Task<int> GetTemperatureAsync()
{
    await Task.Delay(10);
    return 18;
}
example.cs(1,19): error CS0029: Cannot implicitly convert type 'System.Threading.Tasks.Task<int>' to 'int'

The fix is int temperature = await GetTemperatureAsync();. The harder case is a method that returns a plain Task, because there is no result to assign. Calling SaveAsync(412); without await starts the work and moves on; the program may even end before the save finishes. The compiler only warns about it:

example.cs(1,1): warning CS4014: Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the 'await' operator to the result of the call.

Treat CS4014 as a bug report. If you really want to start work without waiting (rarely a good idea), make that explicit by storing the task, or with a discard: _ = SaveAsync(412);. The Testing and Code Quality page shows how to turn such warnings into errors.

Running tasks in parallel

await in a sequence makes each step wait for the previous one. That is right when a step needs the previous result, but wasteful when the steps are independent, such as reading three meters. Start all the tasks first, then wait for all of them with Task.WhenAll: a method that takes several tasks and returns one task that completes when all of them have completed. For Task<T> inputs its result is an array with every result, in the order of the input, not the order of finishing.

Console.WriteLine("One by one:");
int a = await ReadMeterAsync("KE-1", 300);
int b = await ReadMeterAsync("KE-2", 100);
int c = await ReadMeterAsync("KE-3", 200);
Console.WriteLine($"Total: {a + b + c} kWh");

Console.WriteLine("All at once:");
Task<int> t1 = ReadMeterAsync("KE-1", 300);
Task<int> t2 = ReadMeterAsync("KE-2", 100);
Task<int> t3 = ReadMeterAsync("KE-3", 200);
int[] all = await Task.WhenAll(t1, t2, t3);
Console.WriteLine($"Results: {string.Join(", ", all)}");
Console.WriteLine($"Total: {all.Sum()} kWh");

static async Task<int> ReadMeterAsync(string meterId, int ms)
{
    Console.WriteLine($"  start {meterId}");
    await Task.Delay(ms);
    Console.WriteLine($"  done  {meterId}");
    return ms + 12;
}
One by one:
  start KE-1
  done  KE-1
  start KE-2
  done  KE-2
  start KE-3
  done  KE-3
Total: 636 kWh
All at once:
  start KE-1
  start KE-2
  start KE-3
  done  KE-2
  done  KE-3
  done  KE-1
Results: 312, 112, 212
Total: 636 kWh
Timeline: awaiting three meter reads one by one takes 300 plus 100 plus 200 is 600 milliseconds; starting all three and awaiting Task.WhenAll takes as long as the slowest, 300 milliseconds. One by one about 600 ms KE-1 300 ms KE-2 KE-3 200 ms All at once Task.WhenAll about 300 ms KE-1 300 ms KE-2 KE-3 200 ms all done: await returns 0 ms 300 ms 600 ms
Awaiting one by one adds the waiting times up; starting all tasks first and awaiting Task.WhenAll overlaps them.

The pattern is "start, then await": the tasks start running at the moment ReadMeterAsync is called, so the three waits overlap. With a list of items you can start one task per item with LINQ, for example await Task.WhenAll(meterIds.Select(id => ReadMeterAsync(id))) (see LINQ). For hundreds of items, limit how many run at once, for example with Parallel.ForEachAsync and its MaxDegreeOfParallelism option, so that you do not flood the service you are calling.

When some tasks fail

Task.WhenAll always waits until every task has finished, even when one fails early. If any failed, await then throws the first exception (in input order). The other exceptions are not lost: the combined task's Exception property is an AggregateException that holds all of them, and each input task keeps its own status:

string[] sites = ["portal", "billing", "meter-api"];
Task<string>[] checks = sites.Select(CheckAsync).ToArray();
Task<string[]> all = Task.WhenAll(checks);

try
{
    await all;
}
catch (Exception ex)
{
    Console.WriteLine($"await threw: {ex.Message}");
    Console.WriteLine($"Failures in total: {all.Exception!.InnerExceptions.Count}");
}

foreach (var check in checks)
{
    string outcome = check.IsCompletedSuccessfully
        ? check.Result
        : check.Exception!.InnerException!.Message;
    Console.WriteLine($"{check.Status,-16} {outcome}");
}

static async Task<string> CheckAsync(string name)
{
    await Task.Delay(name.Length * 10);
    if (name != "portal") throw new HttpRequestException($"{name} is down");
    return $"{name} is up";
}
await threw: billing is down
Failures in total: 2
RanToCompletion  portal is up
Faulted          billing is down
Faulted          meter-api is down

WhenAny and WhenEach

Task.WhenAny completes as soon as the first of its tasks completes, and gives you that task (not its result, so you await it once more). It suits "ask two mirrors, use the fastest answer":

Task<string> primary = FetchTariffAsync("primary", 400);
Task<string> mirror = FetchTariffAsync("mirror", 100);

Task<string> first = await Task.WhenAny(primary, mirror);
Console.WriteLine($"First answer: {await first}");

static async Task<string> FetchTariffAsync(string source, int ms)
{
    await Task.Delay(ms);
    return $"0.27 EUR/kWh from {source}";
}
First answer: 0.27 EUR/kWh from mirror

The slower task keeps running in the background; if its work should stop, cancel it (next section). To handle results in the order they finish, .NET 9 and later offer Task.WhenEach, which you consume with await foreach:

Task<int>[] tasks = [ReadMeterAsync(300), ReadMeterAsync(100), ReadMeterAsync(200)];

await foreach (Task<int> done in Task.WhenEach(tasks))
{
    Console.WriteLine($"Finished: {await done} kWh");
}

static async Task<int> ReadMeterAsync(int ms)
{
    await Task.Delay(ms);
    return ms + 12;
}
Finished: 112 kWh
Finished: 212 kWh
Finished: 312 kWh

Cancellation and timeouts

A user closes the page, a service shuts down, a report takes too long: slow work must be stoppable. .NET uses cooperative cancellation. A CancellationTokenSource is the remote control; the CancellationToken it hands out is a small read-only struct that async methods accept as a parameter and check, or pass on to the methods they call. When the source is cancelled, every method holding the token can see it and stop. Nothing is killed from the outside; each method stops at a point where that is safe.

using var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromMilliseconds(500));

try
{
    await ImportReadingsAsync(cts.Token);
}
catch (OperationCanceledException)
{
    Console.WriteLine("Import cancelled");
}

static async Task ImportReadingsAsync(CancellationToken token)
{
    for (int batch = 1; batch <= 10; batch++)
    {
        await Task.Delay(200, token);   // throws when the token is cancelled
        Console.WriteLine($"Batch {batch} imported");
    }
}
Batch 1 imported
Batch 2 imported
Import cancelled

CancelAfter turns the source into a timeout. The cancelled Task.Delay throws a TaskCanceledException, which derives from OperationCanceledException; catch the base type, so that your code handles every kind of cancellation the same way.

In your own loops you can check the token yourself: token.IsCancellationRequested lets you stop cleanly and keep what you have, and token.ThrowIfCancellationRequested() throws the OperationCanceledException for you:

using var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromMilliseconds(500));
int imported = 0;

for (int batch = 1; batch <= 10; batch++)
{
    if (cts.Token.IsCancellationRequested)
    {
        Console.WriteLine($"Stopping cleanly after {imported} batches");
        break;
    }
    await Task.Delay(200);
    imported++;
}
Stopping cleanly after 3 batches

The convention is to make CancellationToken token (or cancellationToken) the last parameter of every async method that may take a while, often with = default so callers can leave it out, and to pass it on to every async call inside. ASP.NET Core, for example, hands your request handlers a token that is cancelled when the client disconnects.

Timeouts with WaitAsync

When a method does not accept a token, WaitAsync puts a time limit on the waiting. It throws a TimeoutException when the limit passes:

try
{
    string report = await BuildReportAsync().WaitAsync(TimeSpan.FromMilliseconds(100));
    Console.WriteLine(report);
}
catch (TimeoutException ex)
{
    Console.WriteLine($"{ex.GetType().Name}: {ex.Message}");
}

static async Task<string> BuildReportAsync()
{
    await Task.Delay(500);
    return "Outage report ready";
}
TimeoutException: The operation has timed out.

Exceptions and async void

An exception inside an async method does not fly out of the method call. It is stored in the returned task, which becomes Faulted, and it is rethrown when someone awaits that task. That is why ordinary try and catch (see Exceptions and Error Handling) work around an await. Blocking with .Result instead wraps the exception in an AggregateException, another reason to await:

Task<int> failing = LoadTariffAsync();

try { int x = failing.Result; }
catch (Exception ex) { Console.WriteLine($".Result threw: {ex.GetType().Name}"); }

try { int x = await failing; }
catch (Exception ex) { Console.WriteLine($"await threw:   {ex.GetType().Name}: {ex.Message}"); }

static async Task<int> LoadTariffAsync()
{
    await Task.Delay(10);
    throw new InvalidOperationException("Tariff table is empty");
}
.Result threw: AggregateException
await threw:   InvalidOperationException: Tariff table is empty

Why async void is dangerous

An async method may also be declared with return type void. Such an async void method returns nothing that can be awaited: the caller cannot wait for it, cannot tell when it is done, and cannot catch its exceptions. An exception that escapes it is raised on a thread-pool thread (or the synchronization context), where nothing catches it, and that stops the whole process, try block or not:

try
{
    LogReading(-5);
    await Task.Delay(200);
    Console.WriteLine("Never printed");
}
catch (Exception ex)
{
    Console.WriteLine($"Caught: {ex.Message}");   // never runs either
}

static async void LogReading(int kwh)
{
    await Task.Delay(50);
    if (kwh < 0) throw new InvalidOperationException($"Negative reading: {kwh}");
    Console.WriteLine($"Logged {kwh}");
}
Unhandled exception. System.InvalidOperationException: Negative reading: -5

Change the return type to Task and await the call, and the catch block receives the exception as expected. async void exists for one purpose: event handlers, whose delegate type requires void (see Delegates, Lambdas and Events). Even there, wrap the body in try and catch.

Blocking, deadlocks and CPU-bound work

task.Result, task.Wait() and task.GetAwaiter().GetResult() all block the current thread until the task is done. Doing that to an unfinished task ("sync over async") throws away the benefit of async, and in some environments it hangs forever.

Desktop apps (WinForms, WPF) and classic ASP.NET on .NET Framework have a synchronization context: after an await, the rest of the method is sent back to the original thread, such as the UI thread. If that same thread is blocked in .Result, waiting for the method to finish, the method can never continue: a deadlock. The window freezes and stays frozen:

// In a WinForms click handler: hangs forever
private void RefreshButton_Click(object sender, EventArgs e)
{
    string status = LoadStatusAsync().Result;   // blocks the UI thread...
    statusLabel.Text = status;
}

private async Task<string> LoadStatusAsync()
{
    await Task.Delay(500);   // ...and this wants to resume on that same UI thread
    return "All systems normal";
}

// The fix: async all the way up
private async void RefreshButton_Click(object sender, EventArgs e)
{
    statusLabel.Text = await LoadStatusAsync();
}

Console apps and ASP.NET Core have no synchronization context, so the same code does not deadlock there; it "works on my machine" and then hangs in the desktop app. The rule that avoids all of this is async all the way: once a method awaits, its callers await it too, up to Main or the event handler.

CPU-bound work with Task.Run

For a long calculation, await alone changes nothing: the work still occupies the current thread. Task.Run queues the work to the thread pool (a set of worker threads that .NET manages for you) and returns a task you can await. In a desktop app that keeps the window responsive while the calculation runs:

double[] readings = Enumerable.Range(1, 2_000_000)
    .Select(i => (i % 500) * 0.5)
    .ToArray();

Task<double> totalTask = Task.Run(() => readings.Sum());
Console.WriteLine("Summing on a thread-pool thread...");
double total = await totalTask;
Console.WriteLine($"Total: {total:F1} kWh");
Summing on a thread-pool thread...
Total: 249500000.0 kWh

Do not wrap I/O in Task.Run: an async I/O method already frees the thread, and wrapping it only borrows a second thread to wait. In a web server, Task.Run rarely helps at all, because every request already runs on a thread-pool thread.

Async streams: await foreach

Some data arrives in pieces: pages of results from an API, lines from a network stream, tickets from a queue. An async stream is a method that returns IAsyncEnumerable<T> and uses both await and yield return. The consumer reads it with await foreach and can start on the first item before the last one has arrived:

await foreach (string ticket in ReadTicketsAsync())
{
    Console.WriteLine($"Processing {ticket}");
}
Console.WriteLine("Queue empty");

static async IAsyncEnumerable<string> ReadTicketsAsync()
{
    string[][] pages = [["INC-31890", "INC-31891"], ["INC-31892"]];
    foreach (string[] page in pages)
    {
        await Task.Delay(50);   // stands in for fetching the next page
        foreach (string id in page)
        {
            yield return id;
        }
    }
}
Processing INC-31890
Processing INC-31891
Processing INC-31892
Queue empty

It is the asynchronous counterpart of the ordinary foreach over an IEnumerable<T> described on the Loops page.

Async in practice: HttpClient

Most real async code calls other systems. HttpClient (namespace System.Net.Http) is .NET's HTTP client, and all its request methods are async. This sketch is not run here, because the examples on this site have no network access:

// One HttpClient for the whole application: create it once and reuse it.
var http = new HttpClient { BaseAddress = new Uri("https://api.example.com/") };

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));

using HttpResponseMessage response = await http.GetAsync("outages/INC-31902", cts.Token);
response.EnsureSuccessStatusCode();   // throws HttpRequestException on 4xx/5xx
string json = await response.Content.ReadAsStringAsync(cts.Token);
  • Reuse one client. Creating a new HttpClient per request opens new connections each time and can exhaust the available network ports under load. In ASP.NET Core, IHttpClientFactory manages clients for you.
  • Set a timeout. The default HttpClient.Timeout is 100 seconds. Pass a token with a shorter limit, or set Timeout, so one slow service cannot hold up yours.
  • Await the body too. Reading the content is a second asynchronous step; turning the JSON into objects is shown on Files, JSON and the .NET Libraries.

HTTP itself, status codes and API design are covered in the Web & APIs topic.