C# async/await
async and await let C# code start an operation that may take time, return control to the caller, and continue later when the result is ready. They are most useful for I/O: web requests, database calls, file operations, timers, and other work where the program would otherwise sit idle. Used well, async code keeps applications responsive and lets servers handle more requests without creating one blocked thread per operation.
Overview: How async/await Works
An asynchronous C# method usually returns Task or Task<T>. A Task is an object representing work that may not be complete yet. Task means the operation eventually completes with no result value. Task<T> means it eventually produces a value of type T. The async keyword allows a method to use await inside its body and tells the compiler to transform the method into a state machine.
await does not mean “create a new thread.” It means: if the awaited operation is already complete, keep going immediately; if it is not complete, pause this method, return an incomplete task to the caller, and arrange for the rest of the method to resume when the awaited task completes. During that pause, the current thread is free to do other work. That is why async is powerful for I/O-bound work. It avoids blocking a thread while waiting for an external operation.
Under the hood, the compiler splits an async method around each await. Local variables that are needed after the await are stored as fields in a generated state machine. The method returns quickly with a Task. When the awaited operation finishes, the continuation runs and the state machine moves to the next step. If the method returns normally, the task completes successfully. If it throws an exception, the task completes as faulted. If cancellation is requested and an OperationCanceledException is thrown with the matching token, the task is treated as canceled.
Async code is contagious in a good way: callers usually need to become async too. If method A awaits method B, then A normally returns Task or Task<T>, and its caller awaits A. Modern C# allows static async Task Main(), so even console programs can start with asynchronous flow.
Syntax
using System.Threading.Tasks;
async Task<int> GetCountAsync()
{
await Task.Delay(100);
return 5;
}
int count = await GetCountAsync();
| Part | Meaning |
|---|---|
async |
Allows await inside the method and makes the compiler build an async state machine. |
Task |
Represents an asynchronous operation with no result value. |
Task<T> |
Represents an asynchronous operation that eventually produces a T. |
await |
Asynchronously waits for a task, then unwraps its result or rethrows its exception. |
Async suffix |
A naming convention that tells callers a method should usually be awaited. |
Examples
Example 1: Await A Simple Operation
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
Console.WriteLine("Before await");
string message = await MakeMessageAsync();
Console.WriteLine(message);
Console.WriteLine("After await");
}
static async Task<string> MakeMessageAsync()
{
await Task.Delay(10);
return "Finished work";
}
}
Output:
Before await
Finished work
After await
MakeMessageAsync returns a Task<string>. The call starts the method, and await pauses Main until the task completes. After the delay, the returned string is unwrapped and assigned to message. Notice that the code reads in the same order as synchronous code, even though it does not block the thread during the delay.
Example 2: Run Independent Work Concurrently
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
Task<string> profileTask = LoadPartAsync("profile", 30);
Task<string> ordersTask = LoadPartAsync("orders", 20);
Task<string> messagesTask = LoadPartAsync("messages", 10);
string[] parts = await Task.WhenAll(profileTask, ordersTask, messagesTask);
foreach (string part in parts)
{
Console.WriteLine(part);
}
}
static async Task<string> LoadPartAsync(string name, int delayMilliseconds)
{
await Task.Delay(delayMilliseconds);
return $"Loaded {name}";
}
}
Output:
Loaded profile
Loaded orders
Loaded messages
The three tasks are created before the first await, so their delays overlap. Task.WhenAll returns a task that completes when all supplied tasks complete. The result array preserves the order of the input tasks, not the order in which the operations finished. This pattern is useful when independent I/O calls can safely run at the same time.
Example 3: Exceptions Flow Through await
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
try
{
int seats = await ReserveSeatsAsync(5, 8);
Console.WriteLine($"Seats left: {seats}");
}
catch (InvalidOperationException ex)
{
Console.WriteLine("Reservation failed");
Console.WriteLine(ex.Message);
}
}
static async Task<int> ReserveSeatsAsync(int available, int requested)
{
await Task.Delay(10);
if (requested > available)
{
throw new InvalidOperationException("Not enough seats available.");
}
return available - requested;
}
}
Output:
Reservation failed
Not enough seats available.
An exception thrown inside an async method is stored on the returned task. When the caller awaits that task, the original exception is rethrown at the await point. That means normal try/catch blocks still work, but they should usually surround the await, not just the method call that created the task.
Example 4: Cooperative Cancellation
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
using CancellationTokenSource source = new CancellationTokenSource();
source.Cancel();
try
{
await ExportReportAsync(source.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("Export was canceled.");
}
}
static async Task ExportReportAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
await Task.Delay(10, cancellationToken);
Console.WriteLine("Report exported.");
}
}
Output:
Export was canceled.
Cancellation in .NET is cooperative. A CancellationToken does not forcibly stop code; methods must observe it. Here the token is already canceled, so ThrowIfCancellationRequested throws before the delay begins. Many framework async APIs also accept a token and complete as canceled when cancellation is requested.
How async/await Works Step By Step
- The caller invokes an async method. The method begins running synchronously until it reaches an incomplete awaited task.
- The compiler-generated state machine stores the method’s current position and any local variables needed later.
- The async method returns an incomplete
Taskto its caller. The current thread is not held hostage while waiting. - The awaited operation completes later. The runtime schedules the continuation of the async method.
- The state machine resumes after the
await. If the awaited task succeeded,awaitproduces its result. If it faulted,awaitthrows the stored exception. If it was canceled,awaitthrowsOperationCanceledException. - When the async method reaches its end, its returned task is marked completed, faulted, or canceled.
In desktop UI frameworks, await often captures the current synchronization context so continuation code runs back on the UI thread. In ASP.NET Core there is normally no UI context to return to. In reusable library code, ConfigureAwait(false) is sometimes used to avoid capturing a context, but application code should usually start with plain await unless it has a specific reason.
Common Mistakes
Forgetting To await
Task<string> nameTask = GetNameAsync();
Console.WriteLine(nameTask.ToUpper());
This does not compile because nameTask is a Task<string>, not a string. You must await the task to get its result.
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
string name = await GetNameAsync();
Console.WriteLine(name.ToUpperInvariant());
}
static async Task<string> GetNameAsync()
{
await Task.Delay(10);
return "ada";
}
}
Output:
ADA
Blocking On Async Code
using System;
using System.Threading.Tasks;
class Program
{
static void Main()
{
string value = GetValueAsync().Result;
Console.WriteLine(value);
}
static async Task<string> GetValueAsync()
{
await Task.Delay(10);
return "done";
}
}
Output:
done
This program finishes, but the style is risky. .Result and .Wait() block a thread and can cause deadlocks in environments with synchronization contexts, especially older UI and ASP.NET patterns. Prefer async all the way up.
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
string value = await GetValueAsync();
Console.WriteLine(value);
}
static async Task<string> GetValueAsync()
{
await Task.Delay(10);
return "done";
}
}
Output:
done
Using async void For Normal Methods
async void SaveAsync()
{
await Task.Delay(10);
throw new InvalidOperationException("Lost exception");
}
async void should be reserved for event handlers. Callers cannot await it, cannot catch its exceptions normally, and cannot know when it finishes. For ordinary async operations, return Task.
Best Practices
- Return
TaskorTask<T>from async methods. Avoidasync voidexcept for event handlers required by a framework. - Name asynchronous methods with the
Asyncsuffix, such asLoadOrdersAsync. - Use
awaitinstead of.Resultor.Wait(). - Start independent tasks before awaiting them, then combine them with
Task.WhenAll. - Do not use async just to move CPU-bound work elsewhere. For CPU-bound parallel work, consider dedicated threading or parallel APIs.
- Pass
CancellationTokento async APIs when callers may need to cancel the operation. - Catch exceptions around the
awaitthat observes the task, because that is where the exception is rethrown. - Keep async methods small and readable. The state machine preserves locals across awaits, so avoid holding large objects longer than necessary.
Practice Exercises
- Write
GetTemperatureAsyncthat waits briefly and returns an integer temperature. Await it fromMainand printTemperature: 72. - Create three independent async methods that return strings after different delays. Start all three tasks first, then print the results with
Task.WhenAll. - Write an async method that accepts a
CancellationToken. Cancel the token before awaiting the method and catchOperationCanceledException.
Summary
asyncandawaitmake asynchronous work read like normal sequential code.TaskandTask<T>represent operations that may complete later.awaitpauses the current async method without blocking the thread.- The compiler turns async methods into state machines that resume after awaited tasks complete.
- Exceptions and cancellation are stored on tasks and observed when the task is awaited.
- Use
Task.WhenAllfor independent operations that can run concurrently. - Avoid
.Result,.Wait(), and normal-methodasync void.
