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 waypublic 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 Taskpublic 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 callspublic 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 CancellationTokenpublic async Task ExecuteAsync(CancellationToken cancellationToken){// Pass the cancellationToken to async methodsawait _scheduler.ProcessPendingJobsAsync(cancellationToken);} -
Use
IAsyncEnumerable<T>for streaming large or incremental results.// Good: Using IAsyncEnumerable<T> for streamingpublic async IAsyncEnumerable<int> GetNumbersAsync(){for (int i = 0; i < 10; i++){await Task.Delay(100); // Simulate async workyield return i;}} -
Use proper naming: add "Async" suffix for asynchronous methods.
// Good: Naming async methods with Async suffixpublic async Task GetUserInstancesAsync(){// ... async work ...}// Bad: Naming async methods without Async word suffixpublic 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 threadsvar data = GetDataAsync().Result;Task.Delay(1000).Wait();- Causes deadlocks
- Blocks threads
-
Don't use async void
// BADpublic async void Process(){await Task.Delay(1000);}// GOODpublic 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/Oawait Task.Run(() => InstanceService.GetInstanceAsync(taskInstanceId));// GOODawait 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 publicTask<int> GetUserProfileCountAsync(){return Task.FromResult(42);} -
Don't use async in constructors - use factory method or initialization method
// BAD — constructors cannot be asyncpublic class TaskAllocationService{public TaskAllocationService(){await InitializeAsync(); // Not allowed}}// GOOD — use factory method or initialization methodpublic 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.