async and await

C# · Practical · 15 min

All modern .NET I/O (HTTP, databases, files) is async; servers scale only if you use it correctly.

A Task represents work that completes in the future; Task<T> produces a value. An async method can await tasks: execution pauses at the await without blocking the thread, and resumes when the task completes. The method itself returns a Task/Task<T> to its caller immediately.

async Task<string> LoadAsync(string url) => await http.GetStringAsync(url);

Rules of thumb:

  • Name async methods with an Async suffix and return Task, Task<T> or ValueTask<T>. Use async void only for event handlers: exceptions from it can't be awaited and crash the process.
  • Go async all the way. Blocking on a task with .Result or .Wait() wastes a thread and can deadlock in UI apps.
  • To run independent operations concurrently, start them first and then await them together: await Task.WhenAll(t1, t2). Awaiting one after another runs them sequentially.
  • Task.Delay is the async version of Thread.Sleep.
  • Exceptions thrown inside an async method are stored in the task and rethrown at the await.
  • Pass a CancellationToken through to support cancellation (Advanced lesson).

Async is about not blocking threads while waiting for I/O; it doesn't make CPU-bound work faster. For CPU-heavy work, Task.Run moves it to the thread pool.

Top-level programs and static async Task Main can use await directly.

Example

using System.Diagnostics;

static async Task<int> FetchAsync(string name, int ms)
{
    await Task.Delay(ms);             // simulates I/O, no thread blocked
    Console.WriteLine($"{name} done");
    return ms;
}

var sw = Stopwatch.StartNew();
int a = await FetchAsync("A", 300);   // sequential
int b = await FetchAsync("B", 300);
Console.WriteLine($"sequential {sw.ElapsedMilliseconds} ms"); // at least 600 ms

sw.Restart();
Task<int> t1 = FetchAsync("C", 300);  // start both...
Task<int> t2 = FetchAsync("D", 300);
int[] results = await Task.WhenAll(t1, t2); // ...then await together
Console.WriteLine($"concurrent {sw.ElapsedMilliseconds} ms, sum {results.Sum()}");
// concurrent about 300 ms, sum 600

try { await Task.FromException(new InvalidOperationException("boom")); }
catch (InvalidOperationException ex) { Console.WriteLine(ex.Message); } // boom

Remember

  • await pauses the method without blocking the thread
  • Start tasks first, then await Task.WhenAll for concurrency
  • Avoid .Result/.Wait() and async void
  • Exceptions surface at the await

Watch out for

  • Calling an async method without awaiting it, so errors are lost and work may not finish.

Check yourself

  1. Two independent 1-second async calls are awaited one after the other. Roughly how long does it take?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/