EthicsComputerCourses
Join training +91 90532 10052

async & await

C# · Lesson 27 of 43 Intermediate 7 min read Updated

Much of a program's time is spent waiting: for a web server to reply, a file to be read, a database to answer. If the program simply stops while it waits, a desktop app freezes and a web server can handle fewer users. Asynchronous code lets the program start a slow operation, do something else (or free its thread for other work) while waiting, and continue when the result arrives.

C# makes this remarkably easy with the async and await keywords. In this lesson you will write async methods, run several waits at the same time, handle errors and cancellation, and read files and streams asynchronously.

Task, async and await

A Task represents work that will finish in the future. A Task<T> will finish with a result of type T. You mark a method async so that you can use await inside it. await pauses this method until the task completes, without blocking the thread.

C#
Console.WriteLine("1. Order placed");
Task<int> cooking = CookAsync();
Console.WriteLine("3. Laying the table while the food cooks");

int dishes = await cooking;
Console.WriteLine($"5. Served {dishes} dishes");

async Task<int> CookAsync()
{
    Console.WriteLine("2. Cooking started");
    await Task.Delay(500); // pretend this is slow work
    Console.WriteLine("4. Cooking finished");
    return 3;
}
Output
1. Order placed
2. Cooking started
3. Laying the table while the food cooks
4. Cooking finished
5. Served 3 dishes

Follow the numbers. Calling CookAsync() runs it until its first await, printing step 2. At await Task.Delay(500) the method hands back an unfinished Task<int>, so the caller continues and prints step 3. Then await cooking waits for the result. After half a second the cooking method resumes, prints step 4 and returns 3.

Some conventions:

  • An async method returns Task (no result), Task<T> (a result) or IAsyncEnumerable<T> (a stream of results).
  • Its name ends in Async, like File.ReadAllTextAsync and CookAsync.
  • Top-level statements can use await directly, as above.
  • Task.Delay waits without blocking. Never use Thread.Sleep in async code; it blocks the thread.

Waiting for several things at once

The biggest practical win of async code is running independent waits concurrently. Compare doing three slow calls one after another with starting all three and awaiting them together:

C#
using System.Diagnostics;

var timer = Stopwatch.StartNew();
string a = await FetchWeatherAsync("Rohtak");
string b = await FetchWeatherAsync("Jaipur");
string c = await FetchWeatherAsync("Kochi");
Console.WriteLine($"One by one: {timer.Elapsed.TotalSeconds:F0} s");

timer.Restart();
Task<string>[] tasks =
[
    FetchWeatherAsync("Rohtak"),
    FetchWeatherAsync("Jaipur"),
    FetchWeatherAsync("Kochi")
];
string[] reports = await Task.WhenAll(tasks);
Console.WriteLine($"Together: {timer.Elapsed.TotalSeconds:F0} s");
Console.WriteLine(string.Join(", ", reports));

async Task<string> FetchWeatherAsync(string city)
{
    await Task.Delay(1000); // a pretend one-second network call
    return $"{city}: sunny";
}
Output
One by one: 3 s
Together: 1 s
Rohtak: sunny, Jaipur: sunny, Kochi: sunny

In the second half, all three tasks start immediately and wait at the same time, so the total is about one second instead of three. Task.WhenAll returns the results in the same order as the tasks. Task.WhenAny instead finishes as soon as the first task completes, which is useful for timeouts and "fastest server wins" logic. (The times are rounded; your exact timings vary slightly.)

Errors and cancellation

Exceptions thrown inside an async method are stored in its task and re-thrown at the await, so you catch them with a normal try/catch around the await.

Long operations should also be cancellable: a user closes the page, or a request takes too long. .NET uses a CancellationToken for this. You create a CancellationTokenSource, pass its Token down to the async methods, and they stop with an OperationCanceledException when cancellation is requested.

C#
using CancellationTokenSource cts = new(TimeSpan.FromSeconds(1)); // cancel after 1 second

try
{
    await BuildReportAsync(cts.Token);
    Console.WriteLine("Report ready");
}
catch (OperationCanceledException)
{
    Console.WriteLine("Report cancelled: it took too long.");
}

async Task BuildReportAsync(CancellationToken token)
{
    for (int step = 1; step <= 5; step++)
    {
        await Task.Delay(400, token);
        Console.WriteLine($"Step {step} done");
    }
}
Output
Step 1 done
Step 2 done
Report cancelled: it took too long.

Each step takes 0.4 seconds. Steps 1 and 2 finish at 0.4 and 0.8 seconds; at 1 second the token is cancelled, so the third Task.Delay stops early and throws. Almost every async method in .NET accepts an optional CancellationToken; pass it along.

Async files and async streams

File and network methods have async versions. For large files, File.ReadLinesAsync (available since .NET 7) returns an IAsyncEnumerable<string>, an async stream that you consume with await foreach. You can write your own async streams with yield return.

C#
await File.WriteAllTextAsync("menu.txt", "Idli\nDosa\nVada\n");

string[] items = await File.ReadAllLinesAsync("menu.txt");
Console.WriteLine($"{items.Length} items: {string.Join(", ", items)}");

await foreach (string line in File.ReadLinesAsync("menu.txt"))
{
    Console.WriteLine(line.ToUpper());
}

await foreach (int score in LiveScoreAsync())
{
    Console.WriteLine($"Score: {score}");
}

async IAsyncEnumerable<int> LiveScoreAsync()
{
    int score = 0;
    foreach (int runs in new[] { 4, 1, 6 })
    {
        await Task.Delay(300);
        score += runs;
        yield return score;
    }
}
Output
3 items: Idli, Dosa, Vada
IDLI
DOSA
VADA
Score: 4
Score: 5
Score: 11

The same pattern works for web requests. A real program creates one HttpClient and reuses it, then calls methods such as await http.GetStringAsync(url) or await http.GetFromJsonAsync<T>(url). You will see web code from the server side in ASP.NET Core Minimal APIs.

Common mistakes

  • Forgetting await. Calling SaveAsync(); without await starts the work but does not wait for it, and exceptions are lost. The compiler warns with CS4014.
  • async void methods. Exceptions from them cannot be caught by the caller. Return Task instead; async void is only for UI event handlers.
  • Awaiting one by one when the calls are independent. Start them all and use Task.WhenAll.
  • Using Thread.Sleep or .Result inside async code. Both block a thread; use await Task.Delay and await.
  • Thinking async makes CPU work faster. async helps while waiting. Heavy calculations need parallelism (Parallel.For, Task.Run), which is a separate topic.

Key points

  • Task and Task<T> represent work that finishes later; async methods use await to wait without blocking.
  • An async method runs synchronously until its first await, then returns an unfinished task to its caller.
  • Start independent operations together and await them with Task.WhenAll.
  • Exceptions surface at the await; catch them with try/catch.
  • Pass CancellationToken values down so long operations can be stopped.
  • IAsyncEnumerable<T> with await foreach processes streams such as File.ReadLinesAsync.

Exercise

Simulate three payment gateways that each take a different time to answer: "FastPay" (300 ms), "QuickUPI" (800 ms) and "SlowBank" (2000 ms). Write async Task<string> ChargeAsync(string gateway, int ms, CancellationToken token) that waits and returns "<gateway> approved". Start all three with a shared token that cancels after 1 second, use Task.WhenAny to print the first answer, then cancel the rest and report how many tasks were cancelled.

Show solution
C#
using CancellationTokenSource cts = new(TimeSpan.FromSeconds(1));

Task<string>[] attempts =
[
    ChargeAsync("FastPay", 300, cts.Token),
    ChargeAsync("QuickUPI", 800, cts.Token),
    ChargeAsync("SlowBank", 2000, cts.Token)
];

Task<string> first = await Task.WhenAny(attempts);
Console.WriteLine($"First answer: {await first}");

cts.Cancel(); // we have our answer; stop the others

int cancelled = 0;
foreach (Task<string> t in attempts)
{
    try
    {
        await t;
    }
    catch (OperationCanceledException)
    {
        cancelled++;
    }
}
Console.WriteLine($"Cancelled: {cancelled}");

async Task<string> ChargeAsync(string gateway, int ms, CancellationToken token)
{
    await Task.Delay(ms, token);
    return $"{gateway} approved";
}
Output
First answer: FastPay approved
Cancelled: 2

Task.WhenAny returns the task that finished first, and awaiting it gives its result. Cancelling right after that stops QuickUPI and SlowBank, whose awaits then throw OperationCanceledException; FastPay's task had already succeeded, so awaiting it again simply returns its result.

Quick Quiz

1. What does await do inside an async method?

2. Three independent calls each take 1 second. How long does await Task.WhenAll(...) over all three take, roughly?

3. Where is an exception from an async method thrown in the caller?

4. Which return type should an async method that produces no value normally have?

Learn C# with a mentor

Free lessons get you started. Our Programming & Software Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call