1. Introduction
Imagine your code is a checkout line at a store. If you run a synchronous method, the whole thread (your line) waits while the cashier finishes with one customer before starting the next. If the method is asynchronous, you just place your items on the counter and go do other stuff, and when the cashier is done — they'll call you.
An asynchronous method (an async-method) in .NET is a normal method with a special "label" in front of it (async) that lets you use the await operator inside for non-blocking waiting on other async operations (for example, network or file work) without blocking the main thread. That "label" is what turns a boring synchronous shop into a modern no-wait service.
Basic signature of an async method
An async method must be declared with the async modifier in its signature. Here's the most basic example:
public async Task MyAsyncMethod()
{
// Your asynchronous code here
}
Return types of asynchronous methods:
| Return type | Description | Example |
|---|---|---|
|
The method does work asynchronously but doesn't return a result | |
|
The method does work asynchronously and returns a result of type T | |
|
Used only for events. Not recommended elsewhere! | |
|
For high-performance scenarios when the result is often ready synchronously | — |
Important: Don’t use async void anywhere except event handlers! Otherwise you risk losing proper error handling.
2. A simple example of an async method
Back to our practice app (let's say it's a simple console program), and add an async method that fetches data from the network. We'll emulate a delay.
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
Console.WriteLine("Starting async method...");
await MyFakeDownloadAsync();
Console.WriteLine("Async operation finished!");
}
static async Task MyFakeDownloadAsync()
{
Console.WriteLine("Starting download (emulating 2 sec delay)...");
await Task.Delay(2000); // Emulate a long operation
Console.WriteLine("Download completed!");
}
}
Notes on the example:
- The async keyword is present on Main and MyFakeDownloadAsync.
- The await operator can only be used inside methods marked async.
- The method Task.Delay(2000) emulates a long operation (for example, a network request).
- After the await, the thread is not blocked — execution of Main "pauses" until the delay finishes.
3. How an async method works under the hood
When the compiler sees a method with the async keyword, it turns it into a "state machine" that remembers where to resume, local variables, and continues execution after the awaited operation completes.
Here's a very simplified diagram:
+-----------------------------------------------------------+
| Call to an async method (for example, await X) |
+-------------------------+---------------------------------+
|
v
Is the operation completed? (not Task.Completed)
| |
| yes | no
v v
Suspend execution Return immediately
remember context result
(or throw)
|
The asynchronous operation completes...
|
v
The state machine "wakes" the method,
execution continues from after await
You don't need to memorize implementation details: the context (where to resume and local variables) is preserved and automatically restored when the operation finishes.
Where you can (and can't) use async and await
- async is placed before a method (or lambda) declaration.
- Inside an async method you can (and should) use await.
- If you try to write await in a method without async — you'll get a compile error.
- If you mark a method async but there is no await inside — you'll get a warning; the method will run synchronously and return an already completed task.
4. Return value variants and their specifics
Result Task
If a method performs asynchronous actions but doesn't return a value, use Task:
public async Task SaveToFileAsync(string path)
{
await Task.Delay(1000);
// Save something to a file
}
Result Task<T>
If you need a result, use Task<T>:
public async Task<int> CalculateAsync()
{
await Task.Delay(500);
return 42;
}
Call:
int result = await CalculateAsync();
Result async void
Use only for events! For example, a button click handler:
private async void Button_Click(object sender, EventArgs e)
{
await Task.Delay(1000);
// Do something else
}
Why not use async void in normal methods? Because you can't know when such a method finishes, and you won't be able to properly catch exceptions thrown inside it.
Results ValueTask and ValueTask<T>
They appeared for high-performance libraries when the result is often already available synchronously — they help avoid extra Task allocations. Early in your career you can skip them: they're not that common.
5. Example: async calculator returning a result
Let the program compute the sum of two numbers "with a delay", as if it's a very complex operation:
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
Console.WriteLine("Enter the first number:");
int a = int.Parse(Console.ReadLine()!);
Console.WriteLine("Enter the second number:");
int b = int.Parse(Console.ReadLine()!);
Console.WriteLine("Performing calculation...");
int sum = await CalculateSumAsync(a, b);
Console.WriteLine($"Sum: {sum}");
}
static async Task<int> CalculateSumAsync(int x, int y)
{
await Task.Delay(2000); // "Long" operation
return x + y;
}
}
What matters here:
- CalculateSumAsync is an async method that returns Task<int> and contains an await.
- The call in Main uses await (reminder: starting with C# 7.1 the Main method can be async).
- While the sum is being computed, the thread isn't blocked — you can show progress or do other work.
6. Nested methods and nested await
An async method can call another async method, which can call another one. The whole chain continues automatically:
public async Task<int> StartWorkAsync()
{
int data = await GetDataAsync();
int result = await ProcessDataAsync(data);
return result;
}
It's simple: wait for one operation to finish, then the next.
7. Pitfalls and common mistakes
1. Don't forget async!
If you forget async, the compiler will be upset when it sees await inside the method and won't build your code.
2. Don't use async void in normal methods
Async void methods (except event handlers) are black holes for exceptions. Errors in them can "sink" and you might never learn about them.
3. An async method WITHOUT await
You can do this, but it's pointless — the method will run synchronously and return an already completed task.
public async Task DoNothingAsync()
{
// Oops, no await!
}
4. Returning Task from a NON-async method
This is done to unify interfaces: some operations are truly async, some are instantaneous.
public Task<int> Foo()
{
// return 42; // You can't do this!
return Task.FromResult(42); // OK, but there's no asynchrony here.
}
GO TO FULL VERSION