Asynchronous programming in .NET is powerful, but easy to misuse. While `async` and `await` make asynchronous code look cleaner, even experienced developers can fall into traps that affect performance, scalability, or even cause deadlocks.
Here are five common mistakes I’ve seen (or made!) as a .NET developer — and how to fix them.
1. Blocking an async method using `.Result` or `.Wait()`
Calling `.Result` or `.Wait()` on an async method causes the calling thread to block. In UI apps or ASP.NET, this often leads to deadlocks.
// ? Bad : This blocks the thread and may deadlock
var result = GetDataAsync().Result;
Instead, make your method async and use await
:
// ? Good: Safe, clean async
var result = await GetDataAsync();
Tip: Avoid .Result
and .Wait()
in ASP.NET Core or WinForms/WPF apps — always use await
.
2. Missing to use await
inside an async method
Declaring a method as async
But never using await
doesn’t make it asynchronous — and the compiler won’t complain.
// ? Bad: No 'await' here, so it's not really async
public async Task DoWorkAsync()
{
Task.Delay(1000); // This does nothing!
}
You must await
the operation for the method to be truly asynchronous:
// ? Good: Task is awaited properly
public async Task DoWorkAsync()
{
await Task.Delay(1000);
}
Tip: Use await
inside every async
method — or remove async
if it's not needed
3. Returning void
from async methods instead of Task
(except event handlers)
Returning async void
should only be used for event handlers. In other cases, it makes exception handling impossible.
// ? Bad: Async void methods can't be awaited or caught
public async void SaveDataAsync()
{
await Task.Delay(500);
}
Instead, return a Task
:
// ? Good: This can be awaited and caught
public async Task SaveDataAsync()
{
await Task.Delay(500);
}
Tip: Exceptions thrown in async void
methods are uncatchable and crash the app.
4. Making methods async
when they don't need to be
Adding async
without await
is unnecessary and adds overhead.
// ? Bad: No actual async work here
public async Task<int> GetNumberAsync()
{
return 42;
}
This method is better written without async
:
// ? Good: No need to be async here
public Task<int> GetNumberAsync()
{
return Task.FromResult(42);
}
Tip: Save async
for real asynchronous operations like I/O or network calls.
5. Recreating HttpClient
inside every method call
Creating a new An HttpClient
instance in every method can lead to socket exhaustion and poor performance.
// ? Bad: This creates a new socket every time!
public async Task<string> GetData()
{
using var client = new HttpClient();
return await client.GetStringAsync("https://api.example.com/data");
}
The best practice is to reuse a single HttpClient
instance, ideally injected:
// ? Good: Reuse or inject HttpClient
private readonly HttpClient _httpClient;
public MyService(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<string> GetData()
{
return await _httpClient.GetStringAsync("https://api.example.com/data");
}
Tip: HttpClient
is intended to be long-lived and reused to avoid performance issues.
async
/await
is a game changer for responsive and scalable .NET applications. But a few small mistakes can lead to big issues.
Use it wisely. Test it well. And don’t block async code!
Conclusion
Async/await is one of the most important tools in the .NET toolbox. But like any tool, it's best used with care.
If you’ve made one of these mistakes — good news — we all have. What matters is learning from them and writing better async code moving forward!