Skip to main content

    Lesson 12 • Expert Track

    Async & Await

    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.

    What You'll Learn

    • Understand async, await, and the difference between Task and Task<T>
    • See how await pauses without blocking the thread
    • Run operations concurrently and combine them with Task.WhenAll
    • Tell sequential code from concurrent code — and why concurrent is faster
    • Handle exceptions in async methods with try/catch
    • Cancel long-running work with a CancellationToken

    💡 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).

    • 📦 Task — async work that produces no value (the async equivalent of void).
    • 🎁 Task<T> — async work that will eventually produce a value of type T (e.g. Task<int>, Task<string>).
    • ⏸️ await — "wait for this Task and give me its result", but release the thread while waiting instead of blocking.
    • 🏷️ async — a keyword on the method that permits await inside it. It does not, by itself, make anything run on another thread.

    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 AsyncGetDataAsync() — 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.

    Worked example: await a Task<string>

    Read every comment, run it, and watch the method pause at await then resume.

    Try it Yourself »
    C#
    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();   // re
    ...

    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.

    🎯 Your turn: complete an async Task<int>

    Add async, return 42, and await the result — output should read 42.

    Try it Yourself »
    C#
    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 ___ Tas
    ...

    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.

    Worked example: 5s sequential vs 3s concurrent

    Same two tasks, two strategies — watch the elapsed seconds differ.

    Try it Yourself »
    C#
    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");
    
     
    ...

    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.)

    Worked example: await Task.WhenAll(a, b)

    Run two downloads concurrently and sum their sizes from the results array.

    Try it Yourself »
    C#
    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]
    
        
    ...

    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:

    🎯 Your turn: combine two tasks with WhenAll

    Await both prices at once, then sum them — total should be $45.

    Try it Yourself »
    C#
    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:  When
    ...

    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.)

    Worked example: try/catch around await

    An async method throws; the catch handles it and the program continues.

    Try it Yourself »
    C#
    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: Serv
    ...

    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.

    Worked example: cancel after 1 second

    A counting loop is cancelled partway through and stops cleanly.

    Try it Yourself »
    C#
    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 — stoppe
    ...

    🔎 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.

    Worked example: load three services concurrently

    Fetch users, orders, and revenue in parallel and time the saving.

    Try it Yourself »
    C#
    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);
    
         
    ...

    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

    • 💡 Start tasks early, await them late: kick off independent work first, then await — that's what turns sequential code into concurrent code.
    • 💡 Never block on async with .Result or .Wait(): it can deadlock and it wastes a thread. await all the way up the call chain instead.
    • 💡 Avoid async void except for event handlers — use async Task so callers can await and catch exceptions.
    • 💡 Flow a CancellationToken through every async method that accepts one, so long operations can be stopped cleanly.
    • 💡 Suffix async methods with Async (SaveChangesAsync) and use ConfigureAwait(false) in library code to avoid context deadlocks.

    Common Errors (and the fix)

    • "CS4032: The 'await' operator can only be used within an async method": you used await in a method that isn't marked async. Add async to the method signature and make it return Task or Task<T>.
    • "CS1996: Cannot await in the body of a lock statement": you can't await inside a lock. Use an async-aware primitive like SemaphoreSlim.WaitAsync() instead, or restructure so the await happens outside the lock.
    • Deadlock from .Result / .Wait(): blocking on a Task (e.g. GetDataAsync().Result) in a UI or ASP.NET context can freeze forever. The fix is to await instead of blocking, all the way up.
    • Forgetting await (fire-and-forget): writing DoWorkAsync(); with no await starts the task but never observes it — the result is discarded and any exception is swallowed. Add await (the compiler usually warns: "CS4014: this call is not awaited").
    • async void on a normal method: exceptions thrown from an async void method can't be caught by the caller and crash the process. Return Task instead — reserve async void for event handlers only.

    📋 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 Tawait-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.

    🎯 Mini-Challenge: concurrent fetch + time saved

    Run two tasks concurrently with WhenAll; the total should be 65 in ~1.5s.

    Try it Yourself »
    C#
    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 com
    ...

    🎉 Lesson Complete

    • async lets a method use await; it returns Task (no value) or Task<T> (a value)
    • await pauses until a Task finishes without blocking the thread
    • ✅ Start tasks early and await late to run them concurrently — total time ≈ the slowest one
    • Task.WhenAll waits for all tasks; Task.WhenAny waits for the first
    • ✅ async ≠ multithreading — await avoids blocking; Task.Run uses a thread
    • ✅ Handle errors with try/catch around await; cancel with a CancellationToken
    • Next lesson: Delegates & Events — passing methods around and building event-driven code

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service