C# Async and Await Explained: Write Responsive Asynchronous Code
Asynchronous programming allows C# applications to perform operations without unnecessarily blocking the current thread. The async and await keywords make asynchronous code easier to write, read, and maintain.
What is Asynchronous Programming?
Asynchronous programming is a programming approach where an application can start a long-running operation and continue doing other useful work while waiting for that operation to complete.
static async Task DownloadDataAsync()
{
await Task.Delay(2000);
Console.WriteLine("Data downloaded");
}
await DownloadDataAsync();
Why Use async and await?
- Avoid blocking threads during asynchronous operations
- Improve application responsiveness
- Handle network and file operations efficiently
- Write asynchronous code in a readable style
- Improve scalability in server applications
- Simplify asynchronous exception handling
What is a Task?
Task represents an asynchronous operation. A Task can represent an operation that will complete later, while Task
static async Task<int> GetNumberAsync()
{
await Task.Delay(1000);
return 42;
}
int result = await GetNumberAsync();
Console.WriteLine(result);
Using the await Keyword
The await keyword asynchronously waits for a Task to complete. It allows the method to pause at that point without blocking the thread while the awaited operation is in progress.
static async Task<string> GetMessageAsync()
{
await Task.Delay(1000);
return "Hello from async code";
}
string message = await GetMessageAsync();
Console.WriteLine(message);
Async Method Naming
A common C# convention is to add the Async suffix to methods that return a Task or Task
static async Task SaveDataAsync()
{
await Task.Delay(500);
}
static async Task<string> LoadDataAsync()
{
await Task.Delay(500);
return "Data";
}
Returning Values with Task
When an asynchronous method needs to return a value, use Task
static async Task<int> CalculateAsync()
{
await Task.Delay(500);
return 100 + 50;
}
int result = await CalculateAsync();
Console.WriteLine(result);
Calling Multiple Async Operations
When multiple independent asynchronous operations can run at the same time, their tasks can be started first and awaited together.
Task<string> firstTask = GetDataAsync("First");
Task<string> secondTask = GetDataAsync("Second");
string first = await firstTask;
string second = await secondTask;
Console.WriteLine(first);
Console.WriteLine(second);
static async Task<string> GetDataAsync(string name)
{
await Task.Delay(1000);
return $"{name} completed";
}
Using Task.WhenAll
Task.WhenAll is useful when several asynchronous operations should complete before the program continues.
Task<string> firstTask = GetDataAsync("First");
Task<string> secondTask = GetDataAsync("Second");
Task<string> thirdTask = GetDataAsync("Third");
string[] results = await Task.WhenAll(
firstTask,
secondTask,
thirdTask
);
foreach (string result in results)
{
Console.WriteLine(result);
}
Async Exception Handling
Exceptions from awaited asynchronous operations can be handled using normal try-catch blocks.
try
{
await ProcessDataAsync();
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
static async Task ProcessDataAsync()
{
await Task.Delay(500);
throw new InvalidOperationException("Processing failed");
}
Async File Operations
The .NET libraries provide asynchronous APIs for many I/O operations, including file access. These APIs are useful when applications need to avoid blocking while waiting for storage operations.
string content = await File.ReadAllTextAsync("data.txt");
Console.WriteLine(content);
Async HTTP Requests
HttpClient provides asynchronous methods for network requests. This is especially useful for applications that communicate with external APIs.
using HttpClient client = new HttpClient();
string response = await client.GetStringAsync(
"https://example.com"
);
Console.WriteLine(response);
Cancellation with CancellationToken
CancellationToken allows an application to request that an asynchronous operation stop before completion. This is useful for long-running operations and user-cancelled tasks.
static async Task ProcessAsync(CancellationToken token)
{
for (int i = 0; i < 10; i++)
{
token.ThrowIfCancellationRequested();
await Task.Delay(500, token);
Console.WriteLine(i);
}
}
using CancellationTokenSource cts = new();
await ProcessAsync(cts.Token);
Async vs Synchronous Code
| Feature | Synchronous | Asynchronous |
|---|---|---|
| Execution | Waits for operation to finish | Can continue while operation is pending |
| Thread Blocking | May block during I/O | Typically avoids blocking during awaited I/O |
| Syntax | Normal method calls | async and await |
| Common Use | Simple CPU-bound operations | I/O-bound operations |
Async Does Not Automatically Mean Parallel
Asynchronous programming and parallel programming are different concepts. Async code is primarily useful for operations that spend time waiting, while parallelism is about performing multiple units of work at the same time.
Task task1 = Task.Delay(1000);
Task task2 = Task.Delay(1000);
await Task.WhenAll(task1, task2);
Console.WriteLine("Both completed");
Common async and await Mistakes
- Using .Result or .Wait() unnecessarily
- Forgetting to await an asynchronous operation
- Using async for work that does not benefit from asynchronous execution
- Starting independent operations sequentially when they could run concurrently
- Ignoring cancellation for long-running operations
- Creating unnecessary async wrappers around synchronous code
Async Programming Best Practices
- Prefer await over blocking with Result or Wait
- Use asynchronous APIs for I/O-bound operations
- Propagate async all the way through the call chain when appropriate
- Use CancellationToken for operations that may need cancellation
- Use Task.WhenAll for independent asynchronous operations
- Handle expected exceptions at appropriate application boundaries
Real-World Applications
- Calling REST APIs
- Reading and writing files
- Database operations
- Cloud service requests
- Background data processing
- ASP.NET Core web applications
- Network communication
Practice Exercises
- Create a method that returns Task
- Use await with Task.Delay
- Read a file asynchronously
- Call an HTTP endpoint asynchronously
- Run multiple independent tasks with Task.WhenAll
- Handle an asynchronous exception
- Add CancellationToken support to a long-running operation
Conclusion
The async and await features make asynchronous programming much easier to understand and maintain in C#. They are especially valuable for I/O-bound operations such as network requests, file access, and database communication, where avoiding unnecessary thread blocking can improve application responsiveness and scalability.