Async & Await

Reviewed & published by Brayan K

By the end of this lesson you'll be able to write responsive, non-blocking C# with async/await and Task<T> — running slow operations concurrently instead of one-at-a-time, handling their errors, and cancelling them cleanly. This is how real apps stay snappy while they wait on networks, files, and databases.

Part of the free C# course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

💡 Real-World Analogy

Picture ordering food at a busy café. Synchronous (blocking) is standing frozen at the counter staring at the kitchen until your meal is ready — you can't do anything else, and the queue behind you can't move. Asynchronous is placing your order, taking a buzzer, and going to find a seat, reply to a message, or order a coffee while the kitchen cooks. The await keyword is the buzzer going off: you come back and collect your food the moment it's ready. Crucially, you didn't hire a second person to cook — you just stopped wasting your own time standing still. That's the heart of async: freeing the thread to do other work while it waits.

The mental model: Task is a "promise of a result"

A Task is an object that represents work that is in progress and will finish in the future. Think of it as a receipt: you hold it now, and later it can be redeemed for a result (or for an error if the work failed).

That last point surprises people: async is not the same as multithreading. When you await a real I/O operation (a network or disk call), there is often no thread at all tied up during the wait — the OS notifies you when the data arrives. Async is about not blocking; using extra threads is a separate decision.

📊 Async Building Blocks

PieceMeansExample
asyncMethod may use awaitasync Task DoAsync()
awaitWait for a Task, non-blockingawait DoAsync();
TaskAsync work, no resultTask SaveAsync()
Task<T>Async work returning a valueTask<int> GetAsync()
Task.WhenAllWait for ALL tasks at onceawait Task.WhenAll(a, b)
Task.WhenAnyWait for the FIRST to finishawait Task.WhenAny(a, b)
Task.DelayAsync pause (no blocking)await Task.Delay(1000)

By convention, async methods end in Async — GetDataAsync() — so callers know at a glance they should be awaited.

1. async / await Mechanics

Mark a method async and it gains the power to use await. When you await a Task, the method pauses at that line until the work completes — but it does so without freezing the thread, so the rest of your app stays alive. An async method returns Task if it produces no value, or Task<T> if it returns one; the return value you write becomes the Task's result. Read this worked example, run it, then you'll write your own.

using System;
using System.Threading.Tasks;

class Program
{
    // 'async Task Main' lets Main itself use 'await'.
    // A method marked 'async' returns Task (no value) or Task<T> (a value).
    static async Task Main()
    {
        Console.WriteLine("Order placed, waiting for food...");

        // 'await' pauses HERE until BoilEggAsync finishes — but it does NOT
        // block the thread. Control is freed; when the egg is done, we resume.
        string egg = await BoilEggAsync();   // resumes with the returned value

        Console.WriteLine($"Got: {egg}");    // Got: a boiled egg
        Console.WriteLine("Meal served!");
    }

    // Returns Task<string> -> an async method that eventually yields a string.
    static async Task<string> BoilEggAsync()
    {
        Console.WriteLine("  ...egg is boiling (3s)...");
        await Task.Delay(3000);              // simulates 3 seconds of work
        return "a boiled egg";               // becomes the Task's result
    }
}

// ✅ Expected output:
//    Order placed, waiting for food...
//      ...egg is boiling (3s)...
//    Got: a boiled egg
//    Meal served!

Your turn. The program below is almost complete — it just needs the async keyword, a return value, and an await. Fill in the three ___ blanks using the hints, then run it.

using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        // 🎯 YOUR TURN — fill in the blanks marked with ___

        // 3) Call GetNumberAsync and AWAIT its result.
        int value = ___ GetNumberAsync();   // 👉 the keyword that waits:  await

        Console.WriteLine($"The number is {value}");

        // ✅ Expected output:
        //    The number is 42
    }

    // 1) This method must be marked async so it can use await.
    static ___ Task<int> GetNumberAsync()   // 👉 the keyword:  async
    {
        await Task.Delay(500);              // pretend to fetch the number

        // 2) Return the value 42 (it becomes the Task<int>'s result).
        return ___;                         // 👉 a whole number:  42
    }
}

2. Sequential vs Concurrent

Here's the single most valuable async skill. If you await a task immediately, the next task can't start until it finishes — that's sequential, and the times add up. If you start several tasks first (storing each Task) and await them afterwards, they overlap — that's concurrent, and the total is roughly the time of the slowest one. The worked example below times both so you can see 5 seconds shrink to 3.

using System;
using System.Diagnostics;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        // SEQUENTIAL: await the first, THEN start the second.
        // Total ~= 2s + 3s = 5s, because nothing overlaps.
        var sw = Stopwatch.StartNew();
        string toast = await MakeToastAsync();    // wait 2s
        string coffee = await MakeCoffeeAsync();  // THEN wait 3s
        Console.WriteLine($"Sequential: {toast} + {coffee} in {sw.Elapsed.Seconds}s");

        // CONCURRENT: start BOTH first, then await both.
        // Total ~= max(2s, 3s) = 3s, because they overlap.
        sw.Restart();
        Task<string> toastTask = MakeToastAsync();   // started, not awaited yet
        Task<string> coffeeTask = MakeCoffeeAsync(); // started too — both run now
        string t = await toastTask;                  // collect the results
        string c = await coffeeTask;
        Console.WriteLine($"Concurrent: {t} + {c} in {sw.Elapsed.Seconds}s");
    }

    static async Task<string> MakeToastAsync()
    {
        await Task.Delay(2000);     // 2 seconds
        return "toast";
    }

    static async Task<string> MakeCoffeeAsync()
    {
        await Task.Delay(3000);     // 3 seconds
        return "coffee";
    }
}

3. Combining Tasks with Task.WhenAll

Starting tasks then awaiting each one by hand works, but Task.WhenAll is the clean idiom: pass it several tasks and it returns one Task that completes when all of them do. For Task<T> it hands you back an array of results in the same order you supplied the tasks — perfect for fanning out a batch of calls and then combining the answers. (Its sibling Task.WhenAny completes as soon as the first task finishes — handy for racing servers or timeouts.)

using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        // Start both downloads — they run at the same time.
        Task<int> a = DownloadSizeAsync("report.pdf", 1500, 120);
        Task<int> b = DownloadSizeAsync("photo.jpg",   800,  80);

        // Task.WhenAll waits for EVERY task and returns their results
        // together as an array, in the same order you passed them in.
        int[] sizes = await Task.WhenAll(a, b);   // [120, 80]

        int total = sizes[0] + sizes[1];          // 200
        Console.WriteLine($"Downloaded {total} KB in total");
    }

    static async Task<int> DownloadSizeAsync(string name, int ms, int kb)
    {
        await Task.Delay(ms);
        Console.WriteLine($"  {name} done ({kb} KB)");
        return kb;
    }
}

Now you try. Start two price lookups so they run concurrently, await them both with WhenAll, then add the results for the order total. Fill in the three ___ blanks:

using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        // 🎯 YOUR TURN — fill in the blanks marked with ___

        // 1) Start BOTH price lookups so they run concurrently.
        Task<int> cart  = GetPriceAsync("cart",  600, 30);
        Task<int> book  = GetPriceAsync("book",  900, 15);

        // 2) Await BOTH at once with Task.WhenAll -> gives an int[] of results.
        int[] prices = await Task.___(cart, book);   // 👉 method name:  WhenAll

        // 3) Add the two results together for the order total.
        int total = prices[0] ___ prices[1];         // 👉 the + operator

        Console.WriteLine($"Order total: ${total}");

        // ✅ Expected output:
        //    Order total: $45
    }

    static async Task<int> GetPriceAsync(string item, int ms, int price)
    {
        await Task.Delay(ms);
        return price;
    }
}

4. Exceptions in Async Code

When an async method fails, the exception doesn't vanish — it's captured on the Task and re-thrown at the moment you await it. So you handle it with an ordinary try/catch around the await, exactly as you would for synchronous code. (This is also why forgetting await is dangerous: with no await, nothing ever observes the exception.)

using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        // Exceptions in async code use a normal try/catch around the await.
        // The exception is stored on the Task and re-thrown when you await it.
        try
        {
            string data = await FetchDataAsync();
            Console.WriteLine(data);
        }
        catch (InvalidOperationException ex)
        {
            Console.WriteLine($"Caught: {ex.Message}");   // Caught: Server is down
        }

        Console.WriteLine("Program keeps running after handling the error.");
    }

    static async Task<string> FetchDataAsync()
    {
        await Task.Delay(500);
        throw new InvalidOperationException("Server is down");  // simulate failure
    }
}

// ✅ Expected output:
//    Caught: Server is down
//    Program keeps running after handling the error.

5. Cancellation with CancellationToken

Long-running work should be cancellable — a user closes a window, a request times out. The pattern is a CancellationTokenSource that produces a CancellationToken you pass into the async method. The method checks the token (and passes it on to calls like Task.Delay); when cancellation is requested, an OperationCanceledException is thrown, which you catch to stop gracefully instead of crashing.

using System;
using System.Threading;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        // A CancellationToken lets a caller politely STOP a long operation.
        using var cts = new CancellationTokenSource();
        cts.CancelAfter(1000);   // request cancellation after 1 second

        try
        {
            await CountAsync(cts.Token);
        }
        catch (OperationCanceledException)
        {
            Console.WriteLine("Cancelled — stopped early, no crash.");
        }
    }

    static async Task CountAsync(CancellationToken token)
    {
        for (int i = 1; i <= 10; i++)
        {
            // Throws OperationCanceledException if cancellation was requested.
            token.ThrowIfCancellationRequested();
            Console.WriteLine($"  step {i}");
            await Task.Delay(300, token);   // Delay also respects the token
        }
    }
}

// ✅ Expected output:
//      step 1
//      step 2
//      step 3
//      step 4
//    Cancelled — stopped early, no crash.

🔎 Deep Dive: async ≠ multithreading

A common myth is "async makes my code run on another thread." It doesn't, not necessarily. async/await is a mechanism for not blocking while you wait. The big win is for I/O-bound work — calling a web API, reading a file, querying a database — where the program is just waiting for something external. During that wait there's frequently no thread occupied at all; the operating system signals back when the data is ready.

Threads come into play for CPU-bound work — heavy calculation that must actually keep a core busy. For that you reach for Task.Run(...) to push the work onto a background thread, then await the resulting Task. So the rule of thumb is:

// I/O-bound (waiting on the network/disk): await the async API directly
string page = await httpClient.GetStringAsync(url);

// CPU-bound (heavy maths): offload to a thread with Task.Run, then await
int result = await Task.Run(() => CrunchBigNumbers());

In short: await on its own means "don't block while waiting"; Task.Run means "use a background thread." They're different tools for different problems.

Putting It Together: a Dashboard Loader

Here's a small but realistic program that loads three "services" for a dashboard — exactly the pattern behind a real web page that pulls users, orders, and revenue at once. It starts all three concurrently, awaits them with Task.WhenAll, and times the result. You understand every line now.

using System;
using System.Diagnostics;
using System.Threading.Tasks;

class Program
{
    // === Dashboard loader: fetch three "services" concurrently ===
    static async Task Main()
    {
        var sw = Stopwatch.StartNew();

        // Kick off all three at once — they overlap instead of queuing.
        Task<int> users   = FetchAsync("users",   1200, 1500);
        Task<int> orders  = FetchAsync("orders",   900,  320);
        Task<int> revenue = FetchAsync("revenue", 1500,  9800);

        // Await them together; results come back in order.
        int[] r = await Task.WhenAll(users, orders, revenue);

        Console.WriteLine($"Users:   {r[0]}");
        Console.WriteLine($"Orders:  {r[1]}");
        Console.WriteLine($"Revenue: ${r[2]}");

        // Concurrent total time ~= the slowest call (1.5s), not the sum (3.6s).
        Console.WriteLine($"Loaded in {sw.ElapsedMilliseconds / 1000.0:F1}s " +
                          "(sequential would be ~3.6s).");
    }

    static async Task<int> FetchAsync(string name, int ms, int value)
    {
        await Task.Delay(ms);
        Console.WriteLine($"  fetched {name}");
        return value;
    }
}

Because the calls overlap, the total time is roughly the slowest single call (~1.5s), not the sum of all three (~3.6s). That's the entire reason to run them concurrently.

Pro Tips

Common Errors (and the fix)

📋 Quick Reference

TaskCodeNotes
Async method, no resultasync Task DoAsync()Returns Task
Async method, returns valueasync Task<int> GetAsync()return becomes result
Await a resultint x = await GetAsync();Non-blocking wait
Async pauseawait Task.Delay(1000);1 second, no block
Run concurrentlyawait Task.WhenAll(a, b);Waits for all
First to finishawait Task.WhenAny(a, b);Race / timeout
Offload CPU workawait Task.Run(() => Work());Background thread
Cancel workcts.Token / CancelAfter(ms)CancellationToken

Frequently Asked Questions

Q: Does async run my code on another thread?

Not by itself. async/await is about not blocking while you wait on something (usually I/O). For real I/O waits there's often no thread tied up at all. If you need heavy CPU work on a background thread, that's a separate step: await Task.Run(...).

Q: What's the difference between Task and Task<T>?

Task is async work that returns nothing (like void). Task<T> is async work that will produce a value of type T — await-ing it gives you that value, e.g. int n = await GetCountAsync();.

Q: Why shouldn't I just call .Result to get the value?

Because .Result and .Wait() block the current thread until the Task finishes, which wastes the thread and can deadlock in UI/ASP.NET apps. Always await instead — and make the calling method async too.

Q: How do I run several async operations at the same time?

Start them all first (store each Task without awaiting), then await Task.WhenAll(...). They overlap, so the total time is about the slowest one rather than the sum — that's the concurrency win from Section 2.

Mini-Challenge: Fetch Two Results Concurrently

No blanks this time — just a brief and an outline. Start two GetResultAsync calls so they run at the same time, await them both with Task.WhenAll, print the combined total, and (bonus) use a Stopwatch to show how much time you saved versus running them one after another. Run it and check your output against the expected lines in the comments.

using System;
using System.Diagnostics;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        // 🎯 MINI-CHALLENGE: Fetch two results concurrently
        // 1. Start TWO calls to GetResultAsync at the same time
        //    (e.g. one with 1500ms/40, one with 1000ms/25). Don't await yet —
        //    store each in a Task<int> variable so they run concurrently.
        // 2. await Task.WhenAll(...) to get an int[] of both results.
        // 3. Print the combined total of the two numbers.
        // 4. BONUS: use a Stopwatch to show the elapsed time, and note how
        //    much you saved vs running them one-after-another (1500 + 1000).
        //
        // ✅ Expected (1500/40 and 1000/25):
        //    Total: 65
        //    Took ~1.5s (saved ~1.0s vs sequential)

        // your code here
    }

    // Helper provided for you — returns a number after a delay.
    static async Task<int> GetResultAsync(int ms, int value)
    {
        await Task.Delay(ms);
        return value;
    }
}

🎉 Lesson Complete

Practice quiz

What does the await keyword do when applied to a Task?

  • It blocks the current thread until the Task finishes
  • It starts the Task on a brand-new thread
  • It pauses the method until the Task completes, without blocking the thread
  • It cancels the Task after a timeout

Answer: It pauses the method until the Task completes, without blocking the thread. await pauses the async method at that point until the awaited Task completes, but it releases the thread instead of blocking it.

An async method that produces no value should return which type?

  • Task
  • void
  • Task<T>
  • object

Answer: Task. An async method with no result returns Task (the async equivalent of void). Use Task<T> when it returns a value, and reserve async void for event handlers.

What is the difference between Task and Task<T>?

  • Task<T> runs faster than Task
  • Task is only for I/O, Task<T> is only for CPU work
  • There is no difference
  • Task represents async work with no result; Task<T> represents async work that yields a value of type T

Answer: Task represents async work with no result; Task<T> represents async work that yields a value of type T. Task is async work that returns nothing; Task<T> is async work that will produce a value of type T, retrieved by awaiting it.

To run two independent tasks concurrently, you should:

  • await each one immediately on its own line
  • Start both (storing each Task) first, then await them afterwards
  • Wrap both in a single lock statement
  • Call .Result on each task

Answer: Start both (storing each Task) first, then await them afterwards. Starting tasks first and awaiting them later lets them overlap. Awaiting each immediately makes them sequential, so the times add up.

What does Task.WhenAll(a, b) do for two Task<int> values?

  • Completes when all tasks finish and returns their results in an array, in order
  • Returns the result of whichever finishes first
  • Runs the tasks one after another
  • Throws if any task is still running

Answer: Completes when all tasks finish and returns their results in an array, in order. Task.WhenAll waits for every task and, for Task<T>, returns an array of their results in the same order the tasks were supplied.

Task.WhenAny completes when…

  • all of the tasks have finished
  • exactly half of the tasks finish
  • the first of the tasks finishes
  • the slowest task finishes

Answer: the first of the tasks finishes. Task.WhenAny completes as soon as the first task finishes — useful for racing servers or implementing timeouts.

How do you handle an exception thrown inside an awaited async method?

  • You cannot catch exceptions from async methods
  • With an ordinary try/catch around the await
  • Only with a global unhandled-exception handler
  • By calling task.GetException()

Answer: With an ordinary try/catch around the await. The exception is captured on the Task and re-thrown when you await it, so a normal try/catch around the await handles it.

Why should you avoid blocking on a Task with .Result or .Wait()?

  • They are deprecated and removed in .NET
  • They always return null
  • They run the task twice
  • They block the thread, waste it, and can deadlock in UI/ASP.NET contexts

Answer: They block the thread, waste it, and can deadlock in UI/ASP.NET contexts. .Result and .Wait() block the current thread until the Task finishes, wasting it and risking deadlock. Await all the way up instead.

Is async the same as multithreading?

  • Yes, every await starts a new thread
  • No — await avoids blocking while waiting; for I/O there is often no thread tied up at all
  • Yes, but only for Task<T>
  • No, async always uses exactly two threads

Answer: No — await avoids blocking while waiting; for I/O there is often no thread tied up at all. async/await is about not blocking. For real I/O waits there is frequently no thread occupied. Task.Run is the separate tool for CPU-bound work on a background thread.

Which lets a caller politely stop a long-running async operation?

  • Calling Task.Stop()
  • Setting the Task to null
  • A CancellationToken from a CancellationTokenSource
  • Throwing a TimeoutException manually

Answer: A CancellationToken from a CancellationTokenSource. A CancellationTokenSource produces a CancellationToken you pass into the method; when cancellation is requested an OperationCanceledException is thrown to stop gracefully.

Continue this course