.NET Academy

async and await

Write asynchronous code that reads like synchronous code — and understand what await really does.

13 min read

You will learn

  • Write async methods returning Task and Task<T>
  • Explain what happens at an await
  • Run independent awaits concurrently
  • Handle exceptions in async code

Asynchronous code that reads top to bottom

Before async/await, asynchronous code meant callbacks nested inside callbacks. C#'s async and await let you write it like ordinary sequential code:

async Task<decimal> GetOrderTotalAsync(int orderId)
{
    Order order = await db.Orders.FindAsync(orderId);          // wait for the database
    Customer customer = await crm.GetCustomerAsync(order.CustomerId);   // wait for another service
    return order.Total * (1 - customer.Discount);
}
  • async marks a method that contains await.
  • await waits for a task to complete without blocking the thread, then gives you its result.
  • The method returns Task<T> (or Task when there's no value) — its caller can await it in turn.

By convention, async method names end in Async.

What await really does

At each await, if the task isn't finished yet:

  1. The method pauses and returns an incomplete Task to its caller immediately.
  2. The thread is released to do other work (serve another request, keep the UI responsive).
  3. When the awaited operation completes, the method resumes right after the await — possibly on a different thread.

The compiler rewrites your method into a state machine that remembers where it left off and its local variables. If the task is already complete, await simply continues without pausing.

Return types

Return typeUse for
TaskAsync work with no result
Task<T>Async work producing a T
ValueTask<T>Hot paths that often complete synchronously (advanced)
voidOnly event handlers (see next lesson)
IAsyncEnumerable<T>Async streams consumed with await foreach

Top-level programs can use await directly — the editors in this course do.

Sequential vs concurrent

Each await waits before moving on. When operations don't depend on each other, start them all first, then await:

// Sequential: ~2 seconds
var weather = await GetWeatherAsync();
var news = await GetNewsAsync();
 
// Concurrent: ~1 second
Task<Weather> weatherTask = GetWeatherAsync();   // starts now
Task<News> newsTask = GetNewsAsync();            // starts now
await Task.WhenAll(weatherTask, newsTask);
var (weather2, news2) = (weatherTask.Result, newsTask.Result);
 
// Many items: project to tasks, then WhenAll
string[] pages = await Task.WhenAll(urls.Select(url => http.GetStringAsync(url)));

Exceptions

An exception inside an async method is stored in the returned task and rethrown at the await — so ordinary try/catch works:

try
{
    var data = await http.GetStringAsync("https://example.invalid");
}
catch (HttpRequestException ex)
{
    Console.WriteLine($"Request failed: {ex.Message}");
}

With Task.WhenAll, awaiting rethrows the first exception; all of them are available in the task's Exception.InnerExceptions.

async all the way

Once a method awaits, its callers should await it too — up to the top (an ASP.NET Core endpoint, Main, an event handler). Mixing in blocking calls (.Result, .Wait()) defeats the purpose and can deadlock. You'll see these traps in detail next.

Try it

Program.cs
Loading editor…

C# runtime not loaded

Key takeaways

  • async methods return Task/Task<T>; await waits without blocking the thread.
  • At an incomplete await, the method returns to its caller and resumes later via a compiler-generated state machine.
  • Start independent operations first, then await Task.WhenAll(...) to run them concurrently.
  • Exceptions surface at the await — use normal try/catch.

Exercise

FetchAsync simulates a slow web request (200 ms) — don't change it. Implement TotalLengthAsync, which fetches each URL and adds up the lengths of the responses, and FetchAllAsync, which fetches all URLs CONCURRENTLY and returns the responses in the original order.

Program.cs
Loading editor…

C# runtime not loaded

Hint 1

TotalLengthAsync: loop over the URLs; inside the loop, string page = await FetchAsync(url); total += page.Length;

Hint 2

FetchAllAsync: first START every request without awaiting — var tasks = urls.Select(FetchAsync); — then return await Task.WhenAll(tasks);

Show solution
var pages = await FetchAllAsync(new[] { "home", "about" });
Console.WriteLine(string.Join(" | ", pages));

async Task<string> FetchAsync(string url)
{
    await Task.Delay(200);           // a slow network call
    return $"<{url}>";
}

async Task<int> TotalLengthAsync(string[] urls)
{
    int total = 0;
    foreach (var url in urls)
    {
        string page = await FetchAsync(url);
        total += page.Length;
    }
    return total;
}

async Task<string[]> FetchAllAsync(string[] urls)
{
    var tasks = urls.Select(FetchAsync);
    return await Task.WhenAll(tasks);
}

Quiz

4 questions · 3 correct to pass
  1. 1.What happens when execution reaches `await SomethingAsync()` and the task isn't finished yet?

  2. 2.What should an async method that produces no value return?

  3. 3.`var a = await GetAAsync(); var b = await GetBAsync();` — the two calls are independent. How can you make them run concurrently?

  4. 4.How do you catch an exception thrown inside an awaited async method?

To complete this lesson

  • ○ Solve the exercise
  • ○ Pass the quiz (3/4 correct)