Async/Await Patterns: Writing Better Asynchronous Code
Introduction
[Explain benefits of async programming: responsiveness, scalability, resource efficiency; common pitfalls and best practices.]
Prerequisites
- C# or TypeScript knowledge
- Basic understanding of threads/tasks
Core Concepts
| Pattern | Purpose | Use Case |
|---|---|---|
| Fire and Forget | Non-blocking operation without awaiting | Logging, telemetry |
| Sequential Await | Ordered execution | Dependent operations |
| Parallel Await | Concurrent execution | Independent API calls |
| CancellationToken | Graceful termination | Long-running operations |
Step-by-Step Guide (C#)
Step 1: Basic Async Method
public async Task<User> GetUserAsync(int userId)
{
var response = await httpClient.GetAsync($"/users/{userId}");
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<User>();
}
Step 2: Parallel Execution
Wrong (Sequential):
var user = await GetUserAsync(1);
var orders = await GetOrdersAsync(1);
// Total time: T1 + T2
Correct (Parallel):
var userTask = GetUserAsync(1);
var ordersTask = GetOrdersAsync(1);
await Task.WhenAll(userTask, ordersTask);
var user = userTask.Result;
var orders = ordersTask.Result;
// Total time: max(T1, T2)
Step 3: Cancellation Support
public async Task<List<Product>> SearchProductsAsync(string query, CancellationToken cancellationToken)
{
var response = await httpClient.GetAsync($"/products?q={query}", cancellationToken);
return await response.Content.ReadFromJsonAsync<List<Product>>(cancellationToken);
}
// Usage
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var products = await SearchProductsAsync("laptop", cts.Token);
Step 4: ConfigureAwait(false) for Libraries
public async Task<Data> GetDataAsync()
{
var response = await httpClient.GetAsync("/data").ConfigureAwait(false);
return await response.Content.ReadFromJsonAsync<Data>().ConfigureAwait(false);
}
Step 5: Async Streams (IAsyncEnumerable)
public async IAsyncEnumerable<LogEntry> GetLogsAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
{
await foreach (var entry in dbContext.Logs.AsAsyncEnumerable().WithCancellation(cancellationToken))
{
yield return entry;
}
}
// Consumer
await foreach (var log in GetLogsAsync())
{
Console.WriteLine(log.Message);
}
Step 6: Error Handling
try
{
var result = await PerformOperationAsync();
}
catch (HttpRequestException ex)
{
logger.LogError(ex, "HTTP request failed");
throw;
}
catch (OperationCanceledException)
{
logger.LogWarning("Operation was cancelled");
}
TypeScript/JavaScript Patterns
Basic Async/Await
async function fetchUser(userId: number): Promise<User> {
const response = await fetch(`/users/${userId}`);
if (!response.ok) throw new Error('Failed to fetch user');
return await response.json();
}
Parallel Execution
const [user, orders] = await Promise.all([
fetchUser(1),
fetchOrders(1)
]);
Error Handling with Promise.allSettled
const results = await Promise.allSettled([
fetchUser(1),
fetchOrders(1),
fetchPreferences(1)
]);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`Success ${index}:`, result.value);
} else {
console.error(`Error ${index}:`, result.reason);
}
});
Timeout Pattern
function timeout<T>(promise: Promise<T>, ms: number): Promise<T> {
return Promise.race([
promise,
new Promise<T>((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), ms)
)
]);
}
const data = await timeout(fetchData(), 5000);
Common Anti-Patterns
Anti-Pattern 1: Async Void
Wrong:
public async void ProcessData() // Can't catch exceptions
{
await SaveToDbAsync();
}
Correct:
public async Task ProcessDataAsync()
{
await SaveToDbAsync();
}
Anti-Pattern 2: Blocking on Async
Wrong:
var result = GetDataAsync().Result; // Deadlock risk
Correct:
var result = await GetDataAsync();
Anti-Pattern 3: Unnecessary Async
Wrong:
public async Task<int> GetValueAsync()
{
return await Task.FromResult(42); // Unnecessary overhead
}
Correct:
public Task<int> GetValueAsync()
{
return Task.FromResult(42);
}
Performance Optimization
- Use
ValueTask<T>for hot paths with frequent synchronous completion - Avoid capturing context in library code (
ConfigureAwait(false)) - Pool async operations where possible
- Use async streams for large data sets
Testing Async Code
[Fact]
public async Task GetUserAsync_ReturnsUser()
{
var mockHttp = new Mock<HttpMessageHandler>();
mockHttp.Protected()
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new StringContent("{\"id\":1}") });
var client = new HttpClient(mockHttp.Object);
var service = new UserService(client);
var user = await service.GetUserAsync(1);
Assert.NotNull(user);
}
Troubleshooting
Issue: Deadlock in UI applications
Solution: Use ConfigureAwait(false) or ensure async all the way
Issue: Unhandled exception in fire-and-forget
Solution: Always await or handle Task exceptions explicitly
Issue: Memory leak with cancellation
Solution: Dispose CancellationTokenSource properly
Best Practices Summary
- Async all the way (avoid mixing sync/async)
- Always return Task (not async void except event handlers)
- Use cancellation tokens for long operations
- Parallelize independent operations
- Handle exceptions at appropriate levels
Key Takeaways
- Async/await improves responsiveness without blocking threads.
- Parallelization with Task.WhenAll boosts performance.
- CancellationToken enables graceful operation termination.
- Avoid common pitfalls: async void, blocking calls, unnecessary overhead.
Next Steps
- Profile async performance with diagnostics tools
- Implement retry policies with Polly library
- Explore advanced patterns (bulkhead, circuit breaker)
Additional Resources
Which synchronous bottleneck will you optimize first?