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.
Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.
Part of the free C# course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
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.
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.
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.
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.
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.
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.)
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:
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.)
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.
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:
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.
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.
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.
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.
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.
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.