C# Async/Await
Asynchronous programming made simple
β³ What is Async/Await?
Async/Await enables asynchronous programming, allowing your application to perform long-running operations without blocking the main thread. This keeps your UI responsive and improves application performance significantly.
// Simple async method
async Task<string> GetDataAsync()
{
await Task.Delay(1000); // Simulate delay
return "Data loaded!";
}
Output:
Data loaded!
Key Async Concepts
async Keyword
Marks a method as asynchronous
async Task MyMethodAsync()
await Keyword
Waits for async operation to complete
await Task.Delay(1000);
Task Return Type
Represents asynchronous operation
Task<int> GetNumberAsync()
Non-Blocking
Keeps application responsive
// UI stays responsive
πΉ Basic Async Method
An async method in C# is declared using the async keyword and typically returns a Task or Task<T>. The await keyword pauses execution until the asynchronous operation completes, preventing thread blocking and maintaining application responsiveness during I/O-bound or long-running tasks. This pattern is fundamental for scalable applications, allowing efficient resource use while waiting for operations like file reads, network requests, or database queries. It keeps your UI smooth and servers responsive under load.
using System;
using System.Threading.Tasks;
class Program
{
// Async method that returns a Task
static async Task GreetAsync()
{
Console.WriteLine("Starting...");
await Task.Delay(2000); // Wait 2 seconds
Console.WriteLine("Hello after 2 seconds!");
}
static async Task Main()
{
await GreetAsync();
Console.WriteLine("Done!");
}
}
Output:
Starting...
(2 second delay)
Hello after 2 seconds!
Done!
πΉ Async Method with Return Value
Async methods can return values by using the generic Task<T> type, where T is the result type. The await keyword unwraps the final value from the completed task. This is essential for operations that need to produce a result, such as fetching data from an API, reading a configuration file, or querying a database. Returning Task<int>, Task<string>, or other types makes async code both non-blocking and result-oriented, seamlessly integrating asynchronous workflows into your application's logic.
using System;
using System.Threading.Tasks;
class Program
{
// Async method returning a value
static async Task<int> CalculateAsync(int x, int y)
{
Console.WriteLine("Calculating...");
await Task.Delay(1000); // Simulate work
return x + y;
}
static async Task Main()
{
int result = await CalculateAsync(5, 10);
Console.WriteLine($"Result: {result}");
}
}
Output:
Calculating...
(1 second delay)
Result: 15
πΉ Multiple Async Operations
You can execute multiple async operations either sequentially or in parallel, depending on your performance needs. Sequential execution uses await for each task, waiting for completion before starting the next. For independent operations, Task.WhenAll runs them concurrently, dramatically reducing total execution time. This is ideal for calling multiple web services, performing simultaneous database reads, or processing independent data streams. Properly managing concurrency is key to building high-performance, responsive applications that maximize throughput.
using System;
using System.Threading.Tasks;
class Program
{
static async Task<string> FetchData1Async()
{
await Task.Delay(1000);
return "Data 1";
}
static async Task<string> FetchData2Async()
{
await Task.Delay(1000);
return "Data 2";
}
static async Task Main()
{
// Sequential (takes 2 seconds)
string data1 = await FetchData1Async();
string data2 = await FetchData2Async();
Console.WriteLine($"{data1}, {data2}");
// Parallel (takes 1 second)
Task<string> task1 = FetchData1Async();
Task<string> task2 = FetchData2Async();
string[] results = await Task.WhenAll(task1, task2);
Console.WriteLine($"{results[0]}, {results[1]}");
}
}
Output:
Data 1, Data 2
Data 1, Data 2
πΉ Error Handling with Async
Exception handling in async methods uses standard try-catch blocks, similar to synchronous code. Exceptions thrown in an async method are captured within the returned Task and re-thrown when the task is awaited. This provides a consistent and intuitive error-handling model. You can catch specific exceptions like HttpRequestException for network calls or IOException for file operations, ensuring your application gracefully handles failures, logs errors, and provides fallback logic without crashing.
using System;
using System.Threading.Tasks;
class Program
{
static async Task<int> DivideAsync(int a, int b)
{
await Task.Delay(500);
if (b == 0)
throw new DivideByZeroException("Cannot divide by zero!");
return a / b;
}
static async Task Main()
{
try
{
int result = await DivideAsync(10, 2);
Console.WriteLine($"Result: {result}");
result = await DivideAsync(10, 0);
Console.WriteLine($"Result: {result}");
}
catch (DivideByZeroException ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
}
}
Output:
Result: 5
Error: Cannot divide by zero!
πΉ Async Best Practices
Follow established conventions to write robust and maintainable asynchronous code. Suffix async method names with "Async" (e.g., GetDataAsync). Avoid async void except for event handlers. Always await tasks to properly capture exceptions. In library code, use ConfigureAwait(false) to improve performance and prevent potential deadlocks by avoiding forced context marshaling. These practices ensure your async code is reliable, performant, and easier to debug and maintain across different execution contexts.
using System;
using System.Threading.Tasks;
class Program
{
// β
Good: Async suffix, returns Task
static async Task ProcessDataAsync()
{
await Task.Delay(1000);
Console.WriteLine("Data processed");
}
// β
Good: Await the async method
static async Task Main()
{
await ProcessDataAsync();
}
// β Bad: Async void (only use for event handlers)
// static async void ProcessData() { }
// β Bad: Not awaiting async method
// ProcessDataAsync(); // Fire and forget - avoid this
}
Output:
Data processed