Lesson 16 / 25

async and await

Write asynchronous methods with Task and await without blocking threads.

Waiting without blocking

Most server and UI work waits on I/O: HTTP calls, database queries, files. async/await lets a method wait for that I/O without blocking a thread. An async method returns Task (no result), Task<T> (a result) or occasionally ValueTask<T>. When execution reaches await on an operation that has not completed, the method returns to its caller and the thread is freed to do other work; when the operation completes, the rest of the method resumes, possibly on a different thread pool thread. The compiler rewrites the method into a state machine to make this possible. The rules: use async all the way up the call chain; never block on tasks with .Result or .Wait(), which wastes threads and can deadlock in some contexts; avoid async void except for event handlers, because exceptions from it cannot be caught by the caller; and name async methods with an Async suffix.

The thread is released while waiting

At await the thread goes back to the pool; when I/O completes, a thread picks up the rest of the method.

A timeline with a thread bar that stops at a pause marker, a dotted waiting span for I/O, and a second thread bar resuming afterwards.
Figure 6.1 — An async method yields its thread during I/O.

Calling an API asynchronously

await unwraps the result and rethrows exceptions as if the code were synchronous.

using System.Net.Http.Json;

var http = new HttpClient { BaseAddress = new Uri("https://api.example.com/") };

try
{
    var user = await GetUserAsync(http, 42);
    Console.WriteLine(user?.Name ?? "not found");
}
catch (HttpRequestException ex)
{
    Console.WriteLine($"Request failed: {ex.StatusCode}");
}

static async Task<User?> GetUserAsync(HttpClient http, int id)
{
    using var response = await http.GetAsync($"users/{id}");
    if (response.StatusCode == System.Net.HttpStatusCode.NotFound) return null;
    response.EnsureSuccessStatusCode();
    return await response.Content.ReadFromJsonAsync<User>();
}

record User(int Id, string Name);

Async is not parallel by itself

Awaiting three calls one after another still takes the sum of their times. await frees the thread; it does not start other work. To run independent operations concurrently, start them first and await them together.

Quick check: What happens to the thread when an async method awaits an HTTP call that is still in progress?

  • It sleeps until the call finishes
  • It throws a TimeoutException
  • It returns to the thread pool to do other work
  • It spins in a loop checking the result
Answer

It returns to the thread pool to do other work — await releases the thread; the remainder of the method resumes when the operation completes.