Skip to main content

Async/Await — Practical Guide (C# / .NET 8)

Practical, production-ready best practices for async/await in C#. Short, example-driven DOs and DON'Ts aligned with .NET 8 / ASP.NET Core usage.


✅ DOs (Best practices) — with examples

  1. Use "async all the way" — make callers async instead of blocking.

    // Good: Async all the way
    public async Task<Instance> GetPatentInstanceAsync(Guid instanceId)
    {
    var patent = await InstanceService.GetInstanceAsync(instanceId);
    return patent;
    }
    • Avoid mixing sync and async
    • Prevents deadlocks
  2. Return Task or Task<T> (not void)

    // Good: Returns Task
    public Task ValidateInstanceAsync(Guid instanceId)
    {
    Instance userInstance = await InstanceService.GetInstanceAsync(instanceId);
    bool isValidInstance = userInstance.Id != null && userInstance.Id != Guid.Empty;
    }

    // Good: Returns Task<T>
    public async Task<string> GetUserProfileInstanceAsync(Guid instanceId)
    {
    return await InstanceService.GetInstanceAsync(instanceId)?.Name;
    }

    • Enables proper exception handling
    • Supports awaiting and composition
  3. Parallelize independent calls with Task.WhenAll.

    // Good: Use Task.WhenAll to parallelize independent async calls
    public async Task LoadDataAsync(Guid userInstanceId, Guid taskInstanceId)
    {
    var userInstance = InstanceService.GetInstanceAsync(userInstanceId);
    var taskTask = InstanceService.GetInstanceAsync(taskInstanceId);
    await Task.WhenAll(userInstance, taskTask);
    }
    • Faster response time
    • Efficient resource usage
  4. Propagate CancellationToken through async call chains.

    // Good: Propagating CancellationToken
    public async Task ExecuteAsync(CancellationToken cancellationToken)
    {
    // Pass the cancellationToken to async methods
    await _scheduler.ProcessPendingJobsAsync(cancellationToken);
    }
  5. Use IAsyncEnumerable<T> for streaming large or incremental results.

    // Good: Using IAsyncEnumerable<T> for streaming
    public async IAsyncEnumerable<int> GetNumbersAsync()
    {
    for (int i = 0; i < 10; i++)
    {
    await Task.Delay(100); // Simulate async work
    yield return i;
    }
    }
  6. Use proper naming: add "Async" suffix for asynchronous methods.

    // Good: Naming async methods with Async suffix
    public async Task GetUserInstancesAsync()
    {
    // ... async work ...
    }

    // Bad: Naming async methods without Async word suffix
    public async Task GetUserInstances()
    {
    // ... async work ...
    }

❌ DON'Ts (Common mistakes) — with examples

  1. Don't block on async work with .Result or .Wait().

    // BAD — can deadlock or block threads 
    var data = GetDataAsync().Result;

    Task.Delay(1000).Wait();
    • Causes deadlocks
    • Blocks threads
  2. Don't use async void

    // BAD 
    public async void Process()
    {
    await Task.Delay(1000);
    }

    // GOOD
    public async Task ProcessAsync()
    {
    await Task.Delay(1000);
    }
    • Exceptions crash the process
    • Cannot be awaited
  3. Don't wrap async I/O in Task.Run on server apps.

     // BAD — wastes thread pool, no benefit for I/O 
    await Task.Run(() => InstanceService.GetInstanceAsync(taskInstanceId));

    // GOOD
    await InstanceService.GetInstanceAsync(taskInstanceId);;

  4. Don't "fake" async for real I/O methods (except for simple cached/static values).

    // BAD — fakes async, wastes resources 

    // BAD: misleading for I/O methods public
    Task<int> GetUserProfileCountAsync()
    {
    return Task.FromResult(42);
    }
  5. Don't use async in constructors - use factory method or initialization method

     // BAD — constructors cannot be async
    public class TaskAllocationService
    {
    public TaskAllocationService()
    {
    await InitializeAsync(); // Not allowed
    }
    }

    // GOOD — use factory method or initialization method
    public class TaskAllocationService
    {
    private TaskAllocationService() { }
    public static async Task<TaskAllocationService> CreateAsync()
    {
    var service = new TaskAllocationService();
    await service.InitializeAsync();
    return service;
    }
    private async Task InitializeAsync()
    {
    // Async initialization logic
    }
    }
    • Deadlock risk

Quick review checklist

  • Use await inside async methods.
  • Name async methods with the Async suffix.
  • Prefer Task or Task<T>, not async void or int.
  • Avoid mixing sync and async in hot paths.

Further reading