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
-
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
-
Return
TaskorTask<T>(notvoid)// 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
-
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
-
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);
} -
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;
}
} -
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
-
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
-
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
-
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);; -
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);
} -
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
TaskorTask<T>, notasync voidorint. - Avoid mixing sync and async in hot paths.