C# async and await

Keep apps responsive and servers scalable with asynchronous code: write async methods that return Task, await them, run work in parallel with Task.WhenAll, cancel with CancellationToken, and call web APIs with HttpClient.

Lesson 21 of 43Advanced17 min read

Many operations spend most of their time waiting: for a web API to reply, a database query to finish, a file to be read from disk. If a program simply stops and waits, a desktop app freezes, and a web server ties up a thread that could be serving other users. Asynchronous programming lets the program start the operation, do other work, and continue when the result is ready.

C# makes asynchronous code almost as easy to write as normal code with two keywords. A method marked async can use await to pause until an operation finishes, without blocking the thread. Async methods return a Task (or Task<T> when they produce a value), an object that represents work that will complete in the future.

Async is everywhere in modern .NET: HttpClient, Entity Framework, file APIs and ASP.NET Core all offer ...Async methods. In this lesson you'll write and call async methods, run several operations at the same time with Task.WhenAll, cancel work that takes too long, handle errors, and avoid the classic mistakes that cause deadlocks and lost exceptions.

async methods and await

C#
async Task<string> LoadGreetingAsync()
{
    await Task.Delay(500);               // wait without blocking a thread
    return "Hello from the server";
}

string greeting = await LoadGreetingAsync();
Console.WriteLine(greeting);
  • async allows await inside the method.
  • Task<string> means "will produce a string later"; a method that produces nothing returns Task.
  • await pauses this method until the task finishes and gives you its result. The thread can do other work in the meantime.
  • By convention, async method names end in Async.
  • Top-level statements and static async Task Main() can use await directly.

Running work concurrently

Awaiting one call after another runs them in sequence. To run independent operations at the same time, start them all, then await them together:

C#
Task<string> weather = GetWeatherAsync();
Task<string> news = GetNewsAsync();
string[] results = await Task.WhenAll(weather, news);

If each call takes one second, the sequential version takes two seconds and the WhenAll version about one. Task.WhenAny completes as soon as the first task finishes, which is useful for timeouts and racing several sources.

Cancellation and errors

Long operations should accept a CancellationToken so callers can stop them:

C#
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
try
{
    string page = await http.GetStringAsync(url, cts.Token);
}
catch (OperationCanceledException)
{
    Console.WriteLine("Request timed out");
}

Exceptions thrown inside an async method are stored in its task and rethrown when you await it, so normal try/catch around the await works. ASP.NET Core passes a token to every request, cancelled automatically if the user disconnects.

HttpClient and async streams

HttpClient is the standard way to call web APIs: await http.GetStringAsync(url) for text, and await http.GetFromJsonAsync<Product>(url) to download and decode JSON in one step. Create one HttpClient and reuse it (or use IHttpClientFactory in ASP.NET Core); creating a new one per request can exhaust network sockets.

Async streams, IAsyncEnumerable<T>, produce items over time and are consumed with await foreach, which suits paged API results and reading large data sets.

C# Advanced Syntax

Async syntax

async Task DoWorkAsync() { await Task.Delay(100); }
async Task<int> GetCountAsync() { await Task.Delay(100); return 5; }
int n = await GetCountAsync();
var tasks = ids.Select(id => LoadAsync(id));
var all = await Task.WhenAll(tasks);
var first = await Task.WhenAny(t1, t2);
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
await http.GetStringAsync(url, cts.Token);
var product = await http.GetFromJsonAsync<Product>(url);
await foreach (var item in StreamAsync()) { }

C# Advanced Summary

  • async methods return Task or Task<T> and use await to wait without blocking.
  • Name async methods with an Async suffix; await them all the way up.
  • Task.WhenAll runs independent operations concurrently; Task.WhenAny waits for the first.
  • CancellationToken lets callers cancel work; OperationCanceledException signals it.
  • Never use .Result or .Wait() on tasks, and avoid async void except for event handlers.

C# Advanced Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Sequential versus concurrent

C#
using System.Diagnostics;

async Task<string> FetchAsync(string name, int ms)
{
    await Task.Delay(ms);
    return $"{name} ready";
}

var timer = Stopwatch.StartNew();
string a = await FetchAsync("profile", 1000);
string b = await FetchAsync("orders", 1000);
Console.WriteLine($"{a}, {b} in about {timer.ElapsedMilliseconds / 1000.0:F0}s");

timer.Restart();
Task<string> p = FetchAsync("profile", 1000);
Task<string> o = FetchAsync("orders", 1000);
string[] both = await Task.WhenAll(p, o);
Console.WriteLine($"{string.Join(", ", both)} in about {timer.ElapsedMilliseconds / 1000.0:F0}s");
Output
profile ready, orders ready in about 2s
profile ready, orders ready in about 1s
Line-by-line explanation
  1. Line 5Task.Delay stands in for a network call; it waits without blocking a thread.
  2. Lines 10 to 11Each await finishes before the next call starts, so the times add up.
  3. Lines 15 to 16Both tasks start immediately, before anything is awaited.
  4. Line 17WhenAll waits for both and returns their results in order.
  5. Line 18Exact times vary slightly, so they're rounded to whole seconds.
Example 2

Timeouts with cancellation

C#
async Task<string> SlowReportAsync(CancellationToken token)
{
    for (int step = 1; step <= 5; step++)
    {
        token.ThrowIfCancellationRequested();
        Console.WriteLine($"Step {step}");
        await Task.Delay(400, token);
    }
    return "Report complete";
}

using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(1000));
try
{
    Console.WriteLine(await SlowReportAsync(cts.Token));
}
catch (OperationCanceledException)
{
    Console.WriteLine("Cancelled: the report took too long");
}
Output
Step 1
Step 2
Step 3
Cancelled: the report took too long
Line-by-line explanation
  1. Line 1The method accepts a token so callers can stop it.
  2. Line 5Checking the token between steps stops the work promptly.
  3. Line 7Passing the token to Task.Delay cancels the wait itself.
  4. Line 12The source cancels its token automatically after one second.
  5. Line 17Cancellation arrives as an OperationCanceledException.
  6. Lines 3 to 8Steps start at roughly 0, 400 and 800 ms; the cancel at 1000 ms interrupts the third wait.
Example 3

Calling a JSON API with HttpClient

C#
using System.Net.Http.Json;

using var http = new HttpClient { BaseAddress = new Uri("https://jsonplaceholder.typicode.com/") };

try
{
    var todo = await http.GetFromJsonAsync<Todo>("todos/1");
    Console.WriteLine($"#{todo!.Id}: {todo.Title} (done: {todo.Completed})");

    var ids = new[] { 2, 3 };
    var todos = await Task.WhenAll(ids.Select(id => http.GetFromJsonAsync<Todo>($"todos/{id}")));
    Console.WriteLine($"Loaded {todos.Length} more");
}
catch (HttpRequestException ex)
{
    Console.WriteLine($"Network problem: {ex.Message}");
}

record Todo(int Id, string Title, bool Completed);
Output
#1: delectus aut autem (done: False)
Loaded 2 more
Line-by-line explanation
  1. Line 3One HttpClient is created and reused for every request.
  2. Line 7GetFromJsonAsync downloads the response and decodes it into the record.
  3. Line 11Select starts one request per id, and WhenAll awaits them together.
  4. Line 14Network failures arrive as HttpRequestException when the task is awaited.
  5. Line 19JSON property names are matched to the record ignoring case.

Common C# Advanced Mistakes

Blocking on a task

Don't
string data = LoadAsync().Result;          // blocks the thread and can deadlock in UI apps
Do
string data = await LoadAsync();

async void

Don't
async void SaveAsync() { await db.SaveChangesAsync(); }   // callers can't await it or catch its errors
Do
async Task SaveAsync() { await db.SaveChangesAsync(); }

Awaiting independent calls one by one

Don't
var user = await GetUserAsync(id);
var orders = await GetOrdersAsync(id);     // waits for the user first, for no reason
Do
var userTask = GetUserAsync(id);
var ordersTask = GetOrdersAsync(id);
await Task.WhenAll(userTask, ordersTask);
var (user, orders) = (userTask.Result, ordersTask.Result);   // safe: both have completed

C# Advanced in Real Projects

Async in ASP.NET Core

Every endpoint in a modern ASP.NET Core app is async: it awaits database queries with Entity Framework's ToListAsync, calls other services with HttpClient, and passes the request's CancellationToken along. Because threads aren't blocked while waiting, one server can handle thousands of simultaneous requests. Desktop and mobile apps use async to keep the interface responsive while loading data.

C# Advanced FAQ

What is the difference between Task and Task<T> in C#?

Task represents an operation that finishes later without producing a value, like a method returning void. Task<T> represents an operation that eventually produces a value of type T, which you get by awaiting it: string text = await File.ReadAllTextAsync(path);.

Does async await create a new thread?

Not by itself. While an await waits for something like a network response or a file read, no thread is blocked, and the method continues when the result arrives, possibly on a different thread-pool thread. To run CPU-heavy work on a background thread, use Task.Run explicitly.

Why should I avoid .Result and .Wait() on a task?

They block the current thread until the task finishes, which wastes the thread that async was meant to free. In apps with a UI thread or a synchronisation context, they can deadlock, because the task needs that same blocked thread to finish. Use await instead, and make the calling method async too.

What is the difference between Task.WhenAll and Task.WaitAll?

Task.WhenAll returns a task that completes when all the given tasks have finished, and you await it without blocking. Task.WaitAll blocks the current thread until they're all done. In async code, always use WhenAll.

What does ConfigureAwait(false) do?

It tells await that the rest of the method doesn't need to resume on the original context, such as a UI thread. Library code often uses it to avoid unnecessary switching and certain deadlocks. ASP.NET Core and console apps have no such context, so in application code there you can usually leave it out.

C# Advanced Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Complete the line to wait for the task's result.

int count =  CountUsersAsync();
Multiple choiceeasy

What should an async method that returns no value return?

Predict the outputmedium

Each call takes about 2 seconds. Roughly how long does this take?

await Task.WhenAll(WorkAsync(), WorkAsync(), WorkAsync());
Multiple choicemedium

Why is Task.Delay better than Thread.Sleep in async code?

Multiple choicehard

Which exception signals that an operation was cancelled through a CancellationToken?

C# Advanced Quiz

5 questions to check what you learned.

  1. What naming convention do async methods follow?

    • End with Async
    • Start with Async
    • Use lowercase
    • End with Task
  2. What does Task<T> represent?

    • A thread
    • A list of T
    • A delegate
    • An operation that will produce a T when it completes
  3. What's wrong with calling .Result on a task?

    • It blocks the thread and can cause deadlocks
    • Nothing
    • It returns null
    • It runs the task twice
  4. Which method finishes when the first of several tasks completes?

    • Task.WhenAll
    • Task.WhenAny
    • Task.Run
    • Task.Delay
  5. How should HttpClient be used in an application?

    • Create a new one for every request
    • Only in Main
    • Reuse one instance, or get clients from IHttpClientFactory
    • Wrap each call in Task.Run