On this page
Asynchronous programming with async/await in C#
Why asynchronous programming?
I/O operations (network, disk, database) are slow compared to the CPU. Without async, the thread is blocked waiting for the response, wasting resources:
Synchronous: Thread → [WAITS 500ms] → Processes result
Asynchronous: Thread → Starts I/O → does other work → Resumes when completeIn ASP.NET Core, a server with blocked threads can handle very few concurrent requests. With async/await, the same server can serve thousands.
Task and Task\
Task represents an asynchronous operation. Task<T> represents an asynchronous operation that returns a value:
// Task — operation with no return value
Task task = Task.Delay(1000);
await task;
// Task<T> — operation that returns a value
Task<int> taskWithResult = Task.Run(() => 42);
int result = await taskWithResult;
// Create already-completed tasks (useful in tests and mocks)
Task done = Task.CompletedTask;
Task<int> val = Task.FromResult(42);
Task<string> err = Task.FromException<string>(new Exception("Error"));async / await
async marks a method as asynchronous. await suspends execution without blocking the thread:
// Without async — synchronous, blocks the thread
static string ReadFileSync(string path)
{
return File.ReadAllText(path); // blocks the thread until done
}
// With async — asynchronous, frees the thread while waiting
static async Task<string> ReadFileAsync(string path)
{
return await File.ReadAllTextAsync(path); // frees the thread
}
// Sequential awaiting (slower)
static async Task<(string, string)> SequentialAsync()
{
var r1 = await GetData1Async(); // wait for R1
var r2 = await GetData2Async(); // wait for R2 — total = T1 + T2
return (r1, r2);
}
// Parallel awaiting (faster)
static async Task<(string, string)> ParallelAsync()
{
var t1 = GetData1Async(); // start T1 without await
var t2 = GetData2Async(); // start T2 without await
var (r1, r2) = await Task.WhenAll(t1, t2); // total = max(T1, T2)
return (r1, r2);
}Task.WhenAll and Task.WhenAny
// Task.WhenAll — wait for ALL to complete
static async Task ConcurrentExample()
{
var urls = new[]
{
"https://api.github.com/users/dotnet",
"https://api.github.com/users/microsoft",
"https://api.github.com/users/google",
};
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("User-Agent", "DotNetApp");
// Start all requests concurrently
var tasks = urls.Select(url => http.GetStringAsync(url)).ToList();
string[] responses = await Task.WhenAll(tasks);
for (int i = 0; i < responses.Length; i++)
Console.WriteLine($"URL {i + 1}: {responses[i].Length} bytes");
}
// Task.WhenAny — take the FIRST to complete
static async Task<string> FirstResultAsync(IEnumerable<Task<string>> tasks)
{
Task<string> first = await Task.WhenAny(tasks);
return await first; // unwrap to get the result or rethrow exception
}Asynchronous error handling
// try-catch with await works exactly like synchronous code
static async Task ProcessAsync()
{
try
{
var data = await FetchDataAsync("https://api.example.com/resource");
Console.WriteLine($"Data: {data.Length} bytes");
}
catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
{
Console.WriteLine("Resource not found (404)");
}
catch (TaskCanceledException)
{
Console.WriteLine("Request cancelled or timed out");
}
catch (Exception ex)
{
Console.WriteLine($"Unexpected error: {ex.Message}");
}
finally
{
Console.WriteLine("Cleanup always runs");
}
}
// AggregateException with WhenAll
static async Task HandleWhenAllErrorsAsync()
{
var tasks = new[]
{
Task.FromException<string>(new Exception("Task 1 error")),
Task.FromResult("OK"),
Task.FromException<string>(new Exception("Task 3 error")),
};
try
{
await Task.WhenAll(tasks);
}
catch
{
// Inspect all errors
foreach (var task in tasks.Where(t => t.IsFaulted))
Console.WriteLine($"Error: {task.Exception?.InnerException?.Message}");
}
}CancellationToken
The standard mechanism for cancelling asynchronous operations:
// Standard pattern for cancellable methods
static async Task<List<Article>> GetArticlesAsync(
string category,
CancellationToken ct = default) // always with default
{
using var http = new HttpClient();
var url = $"https://api.example.com/articles?category={category}";
// Pass the token to nested operations
var response = await http.GetAsync(url, ct);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync(ct);
return System.Text.Json.JsonSerializer.Deserialize<List<Article>>(json) ?? new();
}
// Cancel manually
using var cts = new CancellationTokenSource();
// Configure automatic timeout
cts.CancelAfter(TimeSpan.FromSeconds(10));
// Or cancel from another thread
Task.Run(() =>
{
Thread.Sleep(3000);
cts.Cancel();
Console.WriteLine("Cancellation requested");
});
try
{
var articles = await GetArticlesAsync("technology", cts.Token);
Console.WriteLine($"Articles: {articles.Count}");
}
catch (OperationCanceledException)
{
Console.WriteLine("Operation cancelled");
}Async Streams (IAsyncEnumerable\)
For processing asynchronous data sequences element by element:
// Async stream producer
static async IAsyncEnumerable<Reading> ReadSensorsAsync(
string deviceId,
[System.Runtime.CompilerServices.EnumeratorCancellation]
CancellationToken ct = default)
{
while (!ct.IsCancellationRequested)
{
await Task.Delay(1000, ct); // simulates sensor read
yield return new Reading(
DeviceId: deviceId,
Value: Random.Shared.NextDouble() * 100,
Timestamp: DateTime.UtcNow
);
}
}
record Reading(string DeviceId, double Value, DateTime Timestamp);
// Consumer
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
await foreach (Reading reading in ReadSensorsAsync("sensor-01", cts.Token))
{
Console.WriteLine($"[{reading.Timestamp:HH:mm:ss}] Value: {reading.Value:F2}");
}Practice
- Parallel vs sequential: Create a method that simulates 5 HTTP calls with
Task.Delay(1000). Measure execution time for sequential vs parallel withTask.WhenAll. - CancellationToken: Implement
DownloadWithTimeoutAsync(string url, int seconds)that cancels the operation if it exceeds the time limit. - Async stream: Create a
GenerateFibonacciAsync()generator that returns Fibonacci numbers withIAsyncEnumerable<long>, with a 100ms delay between each number.
In the next lesson we will explore records and pattern matching — the most modern C# features for modeling immutable data and expressing conditional logic.
using System.Net.Http;
// Async method — always returns Task or Task<T>
static async Task<string> FetchDataAsync(string url)
{
using var http = new HttpClient();
// await does not block the thread — yields control and resumes when done
string json = await http.GetStringAsync(url);
return json;
}
// Call multiple tasks IN PARALLEL with WhenAll
static async Task RunParallelAsync()
{
var urls = new[]
{
"https://jsonplaceholder.typicode.com/posts/1",
"https://jsonplaceholder.typicode.com/posts/2",
"https://jsonplaceholder.typicode.com/posts/3",
};
// Start all tasks at once (NO await here)
Task<string>[] tasks = urls.Select(u => FetchDataAsync(u)).ToArray();
// Wait for ALL to complete
string[] results = await Task.WhenAll(tasks);
Console.WriteLine($"Received {results.Length} responses");
}
// Async entry point
await RunParallelAsync();
Sign in to track your progress