async & await
Much of a program's time is spent waiting: for a web server to reply, a file to be read, a database to answer. If the program simply stops while it waits, a desktop app freezes and a web server can handle fewer users. Asynchronous code lets the program start a slow operation, do something else (or free its thread for other work) while waiting, and continue when the result arrives.
C# makes this remarkably easy with the async and await keywords. In this lesson you will write async methods, run several waits at the same time, handle errors and cancellation, and read files and streams asynchronously.
Task, async and await
A Task represents work that will finish in the future. A Task<T> will finish with a result of type T. You mark a method async so that you can use await inside it. await pauses this method until the task completes, without blocking the thread.
Console.WriteLine("1. Order placed");
Task<int> cooking = CookAsync();
Console.WriteLine("3. Laying the table while the food cooks");
int dishes = await cooking;
Console.WriteLine($"5. Served {dishes} dishes");
async Task<int> CookAsync()
{
Console.WriteLine("2. Cooking started");
await Task.Delay(500); // pretend this is slow work
Console.WriteLine("4. Cooking finished");
return 3;
}1. Order placed 2. Cooking started 3. Laying the table while the food cooks 4. Cooking finished 5. Served 3 dishes
Follow the numbers. Calling CookAsync() runs it until its first await, printing step 2. At await Task.Delay(500) the method hands back an unfinished Task<int>, so the caller continues and prints step 3. Then await cooking waits for the result. After half a second the cooking method resumes, prints step 4 and returns 3.
Some conventions:
- An async method returns
Task(no result),Task<T>(a result) orIAsyncEnumerable<T>(a stream of results). - Its name ends in
Async, likeFile.ReadAllTextAsyncandCookAsync. - Top-level statements can use
awaitdirectly, as above. Task.Delaywaits without blocking. Never useThread.Sleepin async code; it blocks the thread.
Waiting for several things at once
The biggest practical win of async code is running independent waits concurrently. Compare doing three slow calls one after another with starting all three and awaiting them together:
using System.Diagnostics;
var timer = Stopwatch.StartNew();
string a = await FetchWeatherAsync("Rohtak");
string b = await FetchWeatherAsync("Jaipur");
string c = await FetchWeatherAsync("Kochi");
Console.WriteLine($"One by one: {timer.Elapsed.TotalSeconds:F0} s");
timer.Restart();
Task<string>[] tasks =
[
FetchWeatherAsync("Rohtak"),
FetchWeatherAsync("Jaipur"),
FetchWeatherAsync("Kochi")
];
string[] reports = await Task.WhenAll(tasks);
Console.WriteLine($"Together: {timer.Elapsed.TotalSeconds:F0} s");
Console.WriteLine(string.Join(", ", reports));
async Task<string> FetchWeatherAsync(string city)
{
await Task.Delay(1000); // a pretend one-second network call
return $"{city}: sunny";
}One by one: 3 s Together: 1 s Rohtak: sunny, Jaipur: sunny, Kochi: sunny
In the second half, all three tasks start immediately and wait at the same time, so the total is about one second instead of three. Task.WhenAll returns the results in the same order as the tasks. Task.WhenAny instead finishes as soon as the first task completes, which is useful for timeouts and "fastest server wins" logic. (The times are rounded; your exact timings vary slightly.)
Errors and cancellation
Exceptions thrown inside an async method are stored in its task and re-thrown at the await, so you catch them with a normal try/catch around the await.
Long operations should also be cancellable: a user closes the page, or a request takes too long. .NET uses a CancellationToken for this. You create a CancellationTokenSource, pass its Token down to the async methods, and they stop with an OperationCanceledException when cancellation is requested.
using CancellationTokenSource cts = new(TimeSpan.FromSeconds(1)); // cancel after 1 second
try
{
await BuildReportAsync(cts.Token);
Console.WriteLine("Report ready");
}
catch (OperationCanceledException)
{
Console.WriteLine("Report cancelled: it took too long.");
}
async Task BuildReportAsync(CancellationToken token)
{
for (int step = 1; step <= 5; step++)
{
await Task.Delay(400, token);
Console.WriteLine($"Step {step} done");
}
}Step 1 done Step 2 done Report cancelled: it took too long.
Each step takes 0.4 seconds. Steps 1 and 2 finish at 0.4 and 0.8 seconds; at 1 second the token is cancelled, so the third Task.Delay stops early and throws. Almost every async method in .NET accepts an optional CancellationToken; pass it along.
Async files and async streams
File and network methods have async versions. For large files, File.ReadLinesAsync (available since .NET 7) returns an IAsyncEnumerable<string>, an async stream that you consume with await foreach. You can write your own async streams with yield return.
await File.WriteAllTextAsync("menu.txt", "Idli\nDosa\nVada\n");
string[] items = await File.ReadAllLinesAsync("menu.txt");
Console.WriteLine($"{items.Length} items: {string.Join(", ", items)}");
await foreach (string line in File.ReadLinesAsync("menu.txt"))
{
Console.WriteLine(line.ToUpper());
}
await foreach (int score in LiveScoreAsync())
{
Console.WriteLine($"Score: {score}");
}
async IAsyncEnumerable<int> LiveScoreAsync()
{
int score = 0;
foreach (int runs in new[] { 4, 1, 6 })
{
await Task.Delay(300);
score += runs;
yield return score;
}
}3 items: Idli, Dosa, Vada IDLI DOSA VADA Score: 4 Score: 5 Score: 11
The same pattern works for web requests. A real program creates one HttpClient and reuses it, then calls methods such as await http.GetStringAsync(url) or await http.GetFromJsonAsync<T>(url). You will see web code from the server side in ASP.NET Core Minimal APIs.
Common mistakes
- Forgetting
await. CallingSaveAsync();withoutawaitstarts the work but does not wait for it, and exceptions are lost. The compiler warns withCS4014. async voidmethods. Exceptions from them cannot be caught by the caller. ReturnTaskinstead;async voidis only for UI event handlers.- Awaiting one by one when the calls are independent. Start them all and use
Task.WhenAll. - Using
Thread.Sleepor.Resultinside async code. Both block a thread; useawait Task.Delayandawait. - Thinking async makes CPU work faster.
asynchelps while waiting. Heavy calculations need parallelism (Parallel.For,Task.Run), which is a separate topic.
Key points
TaskandTask<T>represent work that finishes later;asyncmethods useawaitto wait without blocking.- An async method runs synchronously until its first
await, then returns an unfinished task to its caller. - Start independent operations together and await them with
Task.WhenAll. - Exceptions surface at the
await; catch them withtry/catch. - Pass
CancellationTokenvalues down so long operations can be stopped. IAsyncEnumerable<T>withawait foreachprocesses streams such asFile.ReadLinesAsync.
Exercise
Simulate three payment gateways that each take a different time to answer: "FastPay" (300 ms), "QuickUPI" (800 ms) and "SlowBank" (2000 ms). Write async Task<string> ChargeAsync(string gateway, int ms, CancellationToken token) that waits and returns "<gateway> approved". Start all three with a shared token that cancels after 1 second, use Task.WhenAny to print the first answer, then cancel the rest and report how many tasks were cancelled.
Show solution
using CancellationTokenSource cts = new(TimeSpan.FromSeconds(1));
Task<string>[] attempts =
[
ChargeAsync("FastPay", 300, cts.Token),
ChargeAsync("QuickUPI", 800, cts.Token),
ChargeAsync("SlowBank", 2000, cts.Token)
];
Task<string> first = await Task.WhenAny(attempts);
Console.WriteLine($"First answer: {await first}");
cts.Cancel(); // we have our answer; stop the others
int cancelled = 0;
foreach (Task<string> t in attempts)
{
try
{
await t;
}
catch (OperationCanceledException)
{
cancelled++;
}
}
Console.WriteLine($"Cancelled: {cancelled}");
async Task<string> ChargeAsync(string gateway, int ms, CancellationToken token)
{
await Task.Delay(ms, token);
return $"{gateway} approved";
}First answer: FastPay approved Cancelled: 2
Task.WhenAny returns the task that finished first, and awaiting it gives its result. Cancelling right after that stops QuickUPI and SlowBank, whose awaits then throw OperationCanceledException; FastPay's task had already succeeded, so awaiting it again simply returns its result.
Quick Quiz
1. What does await do inside an async method?
await frees the thread while waiting and resumes the method when the task is done.
2. Three independent calls each take 1 second. How long does await Task.WhenAll(...) over all three take, roughly?
The tasks wait concurrently, so the total is close to the longest single wait.
3. Where is an exception from an async method thrown in the caller?
The task stores the exception and re-throws it when awaited.
4. Which return type should an async method that produces no value normally have?
Return Task so callers can await it and catch its exceptions; async void is only for event handlers.
Learn C# with a mentor
Free lessons get you started. Our Programming & Software Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
