Async/Await Patterns: Writing Better Asynchronous Code

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?