C# Async Await — Task, async/await, ConfigureAwait, and ValueTask
In this tutorial, you will learn about C# Async Await. We cover key concepts, practical examples, and best practices to help you master this topic.
C# async/await simplifies asynchronous programming by allowing methods to suspend execution without blocking threads, using Task
What You'll Learn
You will master async/await in C#: writing async methods with Task and Task
Why It Matters
Async/await is fundamental to modern .NET development. It enables servers (ASP.NET Core) to handle thousands of concurrent requests with minimal threads. It keeps UI applications responsive during I/O operations. It improves scalability by not blocking threads during I/O waits. Every networked application — web APIs, database access, file I/O — benefits from async.
Real-World Use
ASP.NET Core controllers use async actions for database queries and external API calls. File I/O operations use async methods for reading/writing without blocking. HttpClient methods are fully async. Entity Framework Core provides async query methods. Azure SDK uses async for all cloud operations.
Learning Path
graph LR
A["27: Pattern Matching"] --> B["29: Async Await"]
B --> C["30: Parallel Programming"]
C --> D["31: Span Memory"]
D --> E["32: Index Ranges"]
style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
style D fill:#4a90d9,stroke:#2c5f8a,color:#fff
style E fill:#4a90d9,stroke:#2c5f8a,color:#fff
Note: Lesson 28 (Records & Structs) is the next title, but content on records/structs is already covered in lessons 15 and 16. This topic order follows the original specification.
Basic Async Method
using System.Net.Http;
async Task<string> DownloadContentAsync(string url)
{
using var client = new HttpClient();
string content = await client.GetStringAsync(url);
return content;
}
// Calling an async method
async Task ProcessAsync()
{
Console.WriteLine("Starting download...");
string content = await DownloadContentAsync("https://example.com");
Console.WriteLine($"Downloaded {content.Length} characters");
}
await ProcessAsync();
Async Return Types
// Task: returns nothing
async Task DoWorkAsync()
{
await Task.Delay(100);
Console.WriteLine("Work done");
}
// Task<T>: returns a value
async Task<int> CalculateAsync()
{
await Task.Delay(50);
return 42;
}
// void: fire-and-forget (only for event handlers)
async void Button_Click(object sender, EventArgs e)
{
await Task.Delay(100);
Console.WriteLine("Button clicked");
}
// ValueTask<T>: for performance-sensitive paths
ValueTask<int> GetCachedValueAsync()
{
if (_cachedValue.HasValue)
return new ValueTask<int>(_cachedValue.Value); // Synchronous completion
return new ValueTask<int>(LoadValueAsync()); // Async path
}
private int? _cachedValue;
private async Task<int> LoadValueAsync()
{
await Task.Delay(10);
_cachedValue = 42;
return _cachedValue.Value;
}
Awaiting Multiple Operations
// Sequential (one at a time)
async Task ProcessSequentialAsync()
{
var result1 = await DoWork1Async();
var result2 = await DoWork2Async();
var result3 = await DoWork3Async();
}
// Concurrent (all at once)
async Task ProcessConcurrentAsync()
{
var task1 = DoWork1Async();
var task2 = DoWork2Async();
var task3 = DoWork3Async();
// All three running concurrently now
await Task.WhenAll(task1, task2, task3);
}
// WhenAny (first to complete)
async Task ProcessWhenAnyAsync()
{
var task1 = DoWork1Async();
var task2 = DoWork2Async();
var completed = await Task.WhenAny(task1, task2);
Console.WriteLine($"First completed: {completed.Id}");
}
Error Handling in Async Code
async Task<string> FetchDataWithRetryAsync(string url)
{
int maxRetries = 3;
for (int attempt = 1; attempt <= maxRetries; attempt++)
{
try
{
using var client = new HttpClient();
return await client.GetStringAsync(url);
}
catch (HttpRequestException ex) when (attempt < maxRetries)
{
Console.WriteLine($"Attempt {attempt} failed: {ex.Message}");
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt)));
}
}
throw new InvalidOperationException($"Failed after {maxRetries} attempts");
}
try
{
string data = await FetchDataWithRetryAsync("https://api.example.com/data");
Console.WriteLine($"Got data: {data.Length} chars");
}
catch (Exception ex)
{
Console.WriteLine($"Failed: {ex.Message}");
}
ConfigureAwait
Controls whether the continuation runs on the captured SynchronizationContext:
// UI application: needs to return to UI thread
async Task UpdateUIAsync()
{
var data = await FetchDataAsync().ConfigureAwait(true); // Default, back to UI thread
textBox.Text = data; // Must be on UI thread
}
// Library code: no need to return to original context
async Task<string> LibraryMethodAsync()
{
// Library code should use ConfigureAwait(false) for performance
var result = await SomeOperationAsync().ConfigureAwait(false);
return result;
}
// ASP.NET Core: ConfigureAwait(false) is unnecessary
// ASP.NET Core does not have a SynchronizationContext
async Task<IActionResult> ApiAction()
{
var data = await _service.GetDataAsync(); // ConfigureAwait(false) not needed
return Ok(data);
}
Async Streams (IAsyncEnumerable)
async IAsyncEnumerable<int> GenerateNumbersAsync()
{
for (int i = 0; i < 10; i++)
{
await Task.Delay(100); // Simulate async work
yield return i;
}
}
async Task ProcessStreamAsync()
{
await foreach (var number in GenerateNumbersAsync())
{
Console.WriteLine($"Received: {number}");
}
}
Cancellation
async Task<string> DownloadWithCancellationAsync(string url, CancellationToken ct)
{
using var client = new HttpClient();
ct.ThrowIfCancellationRequested();
var response = await client.GetAsync(url, ct);
ct.ThrowIfCancellationRequested();
return await response.Content.ReadAsStringAsync(ct);
}
async Task ProcessWithTimeoutAsync()
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
try
{
string data = await DownloadWithCancellationAsync("https://example.com", cts.Token);
Console.WriteLine($"Got data: {data.Length} chars");
}
catch (OperationCanceledException)
{
Console.WriteLine("Operation timed out");
}
}
Async Best Practices
// GOOD: Async all the way
async Task<string> GoodAsync() => await FetchDataAsync();
// BAD: Blocking on async (deadlock risk!)
string BadSync() => FetchDataAsync().Result;
string AlsoBad() => FetchDataAsync().GetAwaiter().GetResult();
// GOOD: ConfigureAwait(false) in libraries
async Task<string> LibraryMethodAsync()
{
await InternalOperationAsync().ConfigureAwait(false);
return await FetchAsync().ConfigureAwait(false);
}
// GOOD: Task.WhenAll for concurrency
async Task ProcessAllAsync()
{
var tasks = items.Select(ProcessItemAsync);
await Task.WhenAll(tasks);
}
Async Void
Only use async void for event handlers:
// Acceptable: UI event handler
async void OnButtonClick(object sender, EventArgs e)
{
try
{
await ProcessAsync();
}
catch (Exception ex)
{
// Handle exception (cannot be caught externally)
Console.WriteLine($"Error: {ex.Message}");
}
}
// Never: async void method that is not an event handler
async void DoSomethingAsync() // BAD!
{
// Exceptions here crash the process
}
Common Mistakes
Mistake 1: Blocking on Async Code
Using .Result or .Wait() on a Task causes deadlocks in UI and ASP.NET Classic contexts. Use await all the way up.
Mistake 2: Forgetting to Await Inside Using Blocks
// BAD: Stream disposed before async read completes
using var stream = File.OpenRead("file.txt");
byte[] data = await stream.ReadAsync(buffer); // OK in C# 8+ with IAsyncDisposable
// GOOD: Ensure using block scope
byte[] data;
using (var ms = new MemoryStream())
{
await stream.CopyToAsync(ms);
data = ms.ToArray();
}
Mistake 3: Not Handling Exceptions in Async Void
Exceptions in async void methods crash the Process because there is no Task to observe the exception.
Mistake 4: Running CPU-Bound Code on the Thread Pool with Task.Run Unnecessarily
Use Task.Run only for CPU-bound work that would block the UI thread. For I/O operations, use async methods directly.
Mistake 5: Not Passing Cancellation Tokens
Long-running operations should accept and respect CancellationToken. This enables graceful shutdown and timeout handling.
Mistake 6: Overusing Task.WhenAll with Too Many Concurrent Tasks
Starting thousands of concurrent tasks can overwhelm the system. Use Parallel.ForEachAsync or partition the work.
Practice Questions
- What happens when you await a Task that has already completed?
- What is the purpose of ConfigureAwait(false)?
- When would you use ValueTask
instead of Task ? - How does cancellation work in async methods?
- Write an async method that downloads data from multiple URLs concurrently and returns the first successful response.
Challenge
Create an async cache with expiration. The cache should fetch data using an async Factory function, cache results with a TTL, and serve cached results without hitting the factory again. Use CancellationToken for timeout support.
FAQ
Mini Project
Create an async web scraper:
using System.Net.Http;
using System.Text.RegularExpressions;
class WebScraper
{
private readonly HttpClient _client = new();
private static readonly Regex _titleRegex = new(@"<title>(.*?)</title>", RegexOptions.IgnoreCase);
public async Task<PageResult> ScrapePageAsync(string url, CancellationToken ct = default)
{
try
{
var response = await _client.GetAsync(url, ct);
response.EnsureSuccessStatusCode();
var html = await response.Content.ReadAsStringAsync(ct);
var title = _titleRegex.Match(html).Groups[1].Value;
var links = ExtractLinks(html, url);
return new PageResult
{
Url = url,
Title = title,
StatusCode = (int)response.StatusCode,
ContentLength = html.Length,
LinkCount = links.Length,
RetrievedAt = DateTime.UtcNow
};
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
return new PageResult
{
Url = url,
Title = $"Error: {ex.Message}",
StatusCode = 0,
Error = ex.Message
};
}
}
public async Task<List<PageResult>> ScrapeMultipleAsync(IEnumerable<string> urls,
int maxConcurrency = 5)
{
var semaphore = new SemaphoreSlim(maxConcurrency);
var tasks = urls.Select(async url =>
{
await semaphore.WaitAsync();
try
{
return await ScrapePageAsync(url);
}
finally
{
semaphore.Release();
}
});
return (await Task.WhenAll(tasks)).ToList();
}
private static string[] ExtractLinks(string html, string baseUrl)
{
var linkRegex = new Regex(@"href=""(https?://[^""]+)""", RegexOptions.IgnoreCase);
return linkRegex.Matches(html)
.Select(m => m.Groups[1].Value)
.Take(10) // Limit for demo
.ToArray();
}
}
class PageResult
{
public string Url { get; set; } = "";
public string Title { get; set; } = "";
public int StatusCode { get; set; }
public int ContentLength { get; set; }
public int LinkCount { get; set; }
public DateTime RetrievedAt { get; set; }
public string? Error { get; set; }
}
var scraper = new WebScraper();
var urls = new[]
{
"https://example.com",
"https://httpbin.org/html",
"https://httpbin.org/status/404"
};
Console.WriteLine("Scraping pages...\n");
var results = await scraper.ScrapeMultipleAsync(urls, maxConcurrency: 3);
foreach (var result in results)
{
Console.WriteLine($"URL: {result.Url}");
Console.WriteLine($" Title: {result.Title}");
Console.WriteLine($" Status: {result.StatusCode}");
Console.WriteLine($" Size: {result.ContentLength:N0} chars");
Console.WriteLine($" Links found: {result.LinkCount}");
if (result.Error != null)
Console.WriteLine($" Error: {result.Error}");
Console.WriteLine();
}
Expected output:
Scraping pages...
URL: https://example.com
Title: Example Domain
Status: 200
Size: 1,256 chars
Links found: 1
URL: https://httpbin.org/html
Title: Htppbin: Anything
Status: 200
Size: 3,742 chars
Links found: 5
URL: https://httpbin.org/status/404
Title: Error: Response status code does not indicate success: 404 (Not Found).
Status: 0
Error: Response status code does not indicate success: 404 (Not Found).
What's Next
You have mastered async/await in C#. The next lesson covers parallel programming: Parallel.For, PLINQ, and concurrent collections for multi-threaded processing.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro