1. Introduction
Before jumping into code, let's go over the motivation and imagine a typical situation. Suppose your program downloads a file from the internet:
// Pseudo-code
var data = DownloadFile("https://example.com/file");
ProcessData(data);
The problem is this: while the download is happening, the program "freezes". No other actions are performed — the user can't move the mouse, click buttons, or even wait without a boring "hung" form.
In the past (and in other languages) to solve this you had to use threads (Thread), tasks (Task), delegates, timers — all of that led to tangled code that's hard to read and maintain. In C# the team decided to make life easier: async with the keywords async and await appeared. Now you can write asynchronous code almost as simply as regular code.
Classic "async pain" without async/await
For contrast, let's see how a long-running operation would look using threads if we wanted to avoid blocking the UI:
// Example without async/await, manual
var thread = new Thread(() =>
{
var data = DownloadFile("https://example.com/file");
Console.WriteLine("File downloaded!");
});
thread.Start();
This approach is pretty crude: you have to manage threads manually, there is no simple way to "await" the result, and handling errors is hard.
2. Asynchrony in C#: syntax
Definition: what are async and await?
async is a modifier that prevents a method from being boring and makes it asynchronous. Such a method usually returns either a Task (or Task<T>), or a ValueTask. It's a promise that the result will come, but a bit later (like an online order — you ordered it and wait).
await is an operator that says: "Stop at this line when you get to it. Wait until the operation finishes, but don't block the thread! Other work can continue."
What does an async method look like?
public async Task MyAsyncMethod()
{
Console.WriteLine("Downloading file...");
var data = await DownloadFileAsync("https://example.com/file"); // Waiting for result asynchronously!
Console.WriteLine("Done!");
}
Note:
- The method has the async modifier.
- Inside the method we use await for an asynchronous operation.
- The method returns a Task (or Task<T> if there's a return value).
Visualization: what happens when you call an async method?
graph LR
A[Call MyAsyncMethod] --> B[Run until await]
B --> C[Call DownloadFileAsync]
C --> D{Waiting}
D --> |File not ready| E[Release thread]
D --> |File ready| F[Continue after await]
F --> G[Method finishes]
- Before the first await the method runs synchronously.
- At the await the method's execution is suspended, control returns to the caller.
- When the async operation completes, execution resumes after the await — as if nothing happened.
3. Example: async download using async/await
Let's make our demo app download text from the internet and print its length without blocking the rest of the code.
using System;
using System.Net.Http;
using System.Threading.Tasks;
class Program
{
// Asynchronous method (returns Task)
public static async Task DownloadAndPrintLengthAsync(string url)
{
Console.WriteLine("Starting download...");
// Use HttpClient — it supports async methods
using (var client = new HttpClient())
{
string data = await client.GetStringAsync(url);
Console.WriteLine($"Download finished! Text length: {data.Length} characters.");
}
Console.WriteLine("Method work finished.");
}
static void Main()
{
// Start the async operation and wait for it to finish
var task = DownloadAndPrintLengthAsync("https://www.example.com");
task.Wait(); // For simple console examples this is fine. In real UI or web apps this call will block and can cause deadlocks.
}
}
Notes:
- DownloadAndPrintLengthAsync is fully async, thanks to async and await.
- Inside the method we await the end of the async string download using await.
- In Main() we start the task and explicitly wait for it with Wait(). In modern C# you can make Main async (async Task Main) and just use await.
4. How it works?
Difference between synchronous and asynchronous code
SYNCHRONOUS VERSION
Console.WriteLine("Start");
string data = client.GetStringAsync(url).Result; // .Result blocks the thread!
Console.WriteLine("Operation finished");
ASYNCHRONOUS VERSION
Console.WriteLine("Start");
string data = await client.GetStringAsync(url); // Thread is not blocked
Console.WriteLine("Operation finished");
How does await work "under the hood"?
When you use await, C# automatically "splits" your method into two (or more) parts: everything before the await, and everything after. When the called async method returns a Task, your method returns to the caller, and when the Task completes — execution continues after the await. All this happens automatically; you don't need to worry about threads and switches manually.
Fun fact: C# "turns" your async method into a state machine where each "await" is a new "return point".
Using async/await in real apps
static async Task Main(string[] args)
{
var downloadTask = DownloadAndPrintLengthAsync("https://www.example.com");
// While downloading, do something else
for (int i = 0; i < 5; i++)
{
Console.WriteLine($"Working... iteration {i}");
await Task.Delay(500); // Pause for 0.5 sec, simulate work
}
await downloadTask; // Wait for the download to finish
}
Now the program downloads something and doesn't block: "live" multitasking, almost like a cat that sleeps and watches its bowl at the same time.
Asynchrony ≠ multithreading
An important distinction often confused by beginners: asynchronous code can run on the same thread! Asynchrony is about not blocking the thread, not about necessarily creating new ones. OS threads are expensive resources! Asynchrony lets you "release" the thread to do other work instead of waiting for long operations (network, file, timers, etc.).
Comparison table: When to choose what?
| Scenario | Thread/Task | async/await |
|---|---|---|
| CPU-bound tasks | Yes | Yes (via Task.Run) |
| I/O-bound tasks | Inefficient | Ideal |
| High concurrency | Hard | Easy |
| Code simplicity | Hardcore | Readable |
5. Useful nuances
Async Main
With modern C# you can make the Main method async!
static async Task Main(string[] args)
{
// All your async code can be awaited right in Main
await DownloadAndAnalyzeFileAsync("https://example.com/file");
}
Common mistake: forgot to await — the task "leaked"
SomeAsyncFunction(); // not awaited, nobody waits for it to finish!
As a result that task will run "into the void" — and if an error occurs you won't even know about it!
When NOT to write async methods
- If there is no asynchronous operation inside the method (no await), don't make it async.
- Don't write methods with async void (except if you are an event handler).
Quick rules and common questions
- Can you use await outside of an async method? No! Always only inside methods marked as async.
- Can you have multiple await in one method? Yes, you can have as many as you want — each is a "wait point".
- What if you want to return a result? Use Task<T> and use return.
- Can you combine multiple async tasks? Of course! You can start multiple tasks in parallel and await them all via Task.WhenAll.
6. Common mistakes when using async/await
Mistake #1: the "fire-and-forget" — calling without await.
DownloadAndPrintLengthAsync("https://www.example.com");
Console.WriteLine("Everything done!"); // Actually the download is still in progress!
The code will NOT wait for the async operation to finish. All async tasks should either be await-ed or explicitly waited on with Wait() (but the latter is dangerous and can cause blocking).
Mistake #2: mixing synchronous and asynchronous code via .Result or .Wait().
This is an anti-pattern that defeats the benefits of asynchrony. In UI or ASP.NET apps this will almost certainly lead to deadlocks: the async task waits for the thread to be free, but the thread is blocked waiting for the task to finish. Remember the mantra: "async all the way".
Mistake #3: Using async void.
async void methods cannot be awaited, and exceptions thrown from them can't be caught with a regular try-catch and typically crash the whole application. The only acceptable use of async void is event handlers (for example, async void Button_Click(...)), where the signature requires it. In all other cases use async Task.
Mistake #4: Unnecessary async on a method without await.
If you mark a method as async but don't use await inside it, the compiler will warn. Such code will run fully synchronously but with extra overhead for creating an unnecessary "state machine". This is misleading and reduces performance.
GO TO FULL VERSION