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:
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; }
Fetching price... Price: 42.0
Three things to notice:
- The method is marked
asyncand returnsTask<decimal>, yet itsreturnstatement returns a plaindecimal— the compiler wraps it in the task. - By convention, async method names end in
Async. - Top-level statements can use
awaitdirectly; the hiddenMainbecomes 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:
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;
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:
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; }
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:
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}"; }
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:
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}"); } }
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.)
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:
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; } }
Reading: 20 Reading: 23 Reading: 26 Reading: 29
Rules of thumb
- Async all the way: if a method awaits something, make it
asyncand have its callers await it too. - Don't block on tasks with
.Resultor.Wait(). It wastes a thread and can deadlock in some environments. Useawait. - Avoid
async voidexcept for event handlers: exceptions from it can't be caught by the caller. ReturnTaskinstead. - 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.
Starting coffee Starting eggs Starting toast toast done coffee done eggs done Breakfast is ready!