Asynchronous Programming with async and await
Waiting without blocking in C#: Task and Task<T>, async methods and await, running work in parallel with Task.WhenAll, cancellation and timeouts, and the common mistakes.
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
awaitof 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
awaitdirectly, as these examples do; the compiler then generates anasync Task Mainfor 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
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
HttpClientper request opens new connections each time and can exhaust the available network ports under load. In ASP.NET Core,IHttpClientFactorymanages clients for you. -
Set a timeout. The default
HttpClient.Timeoutis 100 seconds. Pass a token with a shorter limit, or setTimeout, 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.