Top 5 Mistakes .NET Developers Make When Using async and await

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!