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
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);asyncallowsawaitinside the method.Task<string>means "will produce a string later"; a method that produces nothing returnsTask.awaitpauses 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 useawaitdirectly.
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:
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:
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.
Sequential versus concurrent
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");profile ready, orders ready in about 2s
profile ready, orders ready in about 1sLine-by-line explanation
- Line 5Task.Delay stands in for a network call; it waits without blocking a thread.
- Lines 10 to 11Each await finishes before the next call starts, so the times add up.
- Lines 15 to 16Both tasks start immediately, before anything is awaited.
- Line 17WhenAll waits for both and returns their results in order.
- Line 18Exact times vary slightly, so they're rounded to whole seconds.
Timeouts with cancellation
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");
}Step 1
Step 2
Step 3
Cancelled: the report took too longLine-by-line explanation
- Line 1The method accepts a token so callers can stop it.
- Line 5Checking the token between steps stops the work promptly.
- Line 7Passing the token to Task.Delay cancels the wait itself.
- Line 12The source cancels its token automatically after one second.
- Line 17Cancellation arrives as an OperationCanceledException.
- Lines 3 to 8Steps start at roughly 0, 400 and 800 ms; the cancel at 1000 ms interrupts the third wait.
Calling a JSON API with HttpClient
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);#1: delectus aut autem (done: False)
Loaded 2 moreLine-by-line explanation
- Line 3One HttpClient is created and reused for every request.
- Line 7GetFromJsonAsync downloads the response and decodes it into the record.
- Line 11Select starts one request per id, and WhenAll awaits them together.
- Line 14Network failures arrive as HttpRequestException when the task is awaited.
- Line 19JSON property names are matched to the record ignoring case.
Common C# Advanced Mistakes
Blocking on a task
string data = LoadAsync().Result; // blocks the thread and can deadlock in UI appsstring data = await LoadAsync();async void
async void SaveAsync() { await db.SaveChangesAsync(); } // callers can't await it or catch its errorsasync Task SaveAsync() { await db.SaveChangesAsync(); }Awaiting independent calls one by one
var user = await GetUserAsync(id);
var orders = await GetOrdersAsync(id); // waits for the user first, for no reasonvar userTask = GetUserAsync(id);
var ordersTask = GetOrdersAsync(id);
await Task.WhenAll(userTask, ordersTask);
var (user, orders) = (userTask.Result, ordersTask.Result); // safe: both have completedC# 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.
Complete the line to wait for the task's result.
int count = CountUsersAsync();
What should an async method that returns no value return?
Each call takes about 2 seconds. Roughly how long does this take?
await Task.WhenAll(WorkAsync(), WorkAsync(), WorkAsync());
Why is Task.Delay better than Thread.Sleep in async code?
Which exception signals that an operation was cancelled through a CancellationToken?
C# Advanced Quiz
5 questions to check what you learned.
What naming convention do async methods follow?
- End with Async
- Start with Async
- Use lowercase
- End with Task
What does Task<T> represent?
- A thread
- A list of T
- A delegate
- An operation that will produce a T when it completes
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
Which method finishes when the first of several tasks completes?
- Task.WhenAll
- Task.WhenAny
- Task.Run
- Task.Delay
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