Sitemap

🚀 5 async/await Mistakes Every .NET Developer Should Avoid

5 min readOct 20, 2026

--

Press enter or click to view image in full size

A practical guide with real-world C# examples

Have you ever wondered why your ASP.NET Core API becomes slow when multiple users access it at the same time?

Sometimes, the problem isn’t your database or server capacity. It might be how your code handles asynchronous operations.

The async and await keywords help us write non-blocking code, especially when working with databases, external APIs, files, and other I/O operations.

But using async and await incorrectly can introduce performance problems, unexpected behavior, and unnecessary complexity.

Let’s explore five common mistakes using a real-world e-commerce application.

🛒 Real-world scenario: An e-commerce order API

Imagine a customer opens their order history page.

Your ASP.NET Core API needs to retrieve the customer’s orders from a database and fetch payment information from an external payment service.

Both operations involve waiting for external systems.

Let’s see how to handle these situations correctly.

❌ Mistake 1: Blocking asynchronous code with .Result or .Wait()

The wrong approach

public IActionResult GetOrders()
{
var orders = _orderService
.GetOrdersAsync()
.Result;
    return Ok(orders);
}

Why is this a problem?

The .Result property blocks the current thread until the asynchronous operation finishes.

Imagine 500 requests arriving at your API. If many requests block their threads while waiting for database responses, thread-pool resources can become exhausted, increasing latency and reducing throughput.

ASP.NET Core generally avoids the classic ASP.NET synchronization-context deadlock scenario, but blocking can still hurt scalability.

✅ The better approach

public async Task<IActionResult> GetOrders()
{
var orders = await _orderService
.GetOrdersAsync();
    return Ok(orders);
}

Now, the request can await the database operation without unnecessarily blocking the thread.

Remember: Prefer await over .Result and .Wait() when consuming asynchronous APIs.

❌ Mistake 2: Using async void instead of async Task

The wrong approach

public async void SaveOrderAsync()
{
await _repository.SaveAsync();
}

What’s wrong here?

A caller cannot await this method, and it cannot observe its completion through a returned task. Exceptions are also harder to handle correctly.

✅ The better approach

public async Task SaveOrderAsync()
{
await _repository.SaveAsync();
}

Now the calling code can wait for the operation and handle errors:

try
{
await SaveOrderAsync();
}
catch (Exception ex)
{
// Log or handle the error appropriately.
throw;
}

When should you use async void?

Generally, reserve it for event handlers that require a void return type. For regular application methods, prefer Task or Task<T>.

❌ Mistake 3: Waiting for independent operations one by one

Suppose the order page needs both order details and payment information.

These operations are independent, so they don’t necessarily need to run sequentially.

The slower approach

var orders = await _orderService
.GetOrdersAsync();
var payments = await _paymentService
.GetPaymentsAsync();

If fetching orders takes 2 seconds and fetching payments takes 3 seconds, the total waiting time could be approximately 5 seconds.

✅ The better approach

var ordersTask = _orderService
.GetOrdersAsync();
var paymentsTask = _paymentService
.GetPaymentsAsync();
await Task.WhenAll(ordersTask, paymentsTask);var orders = await ordersTask;
var payments = await paymentsTask;

Now both operations can make progress concurrently. If they take approximately 2 and 3 seconds respectively, the total waiting time may be closer to 3 seconds, excluding overhead.

Get Sachin Ghadi’s stories in your inbox

Join Medium for free to get updates from this writer.

Important: Use this pattern only when the operations are independent and the underlying services support concurrent calls. If one operation depends on the result of another, await them in sequence.

Also, avoid launching an unbounded number of concurrent database operations, particularly when using a shared Entity Framework Core DbContext, which isn't thread-safe.

❌ Mistake 4: Thinking async automatically makes everything faster

Consider this method:

public async Task<int> CalculateTotalAsync()
{
int total = 0;
    for (int i = 0; i < 1_000_000; i++)
{
total += i;
}
return total;
}

The method uses async, but it doesn't await any asynchronous operation.

The compiler will warn that the method lacks await and will execute the CPU-bound loop synchronously.

✅ The better approach

For a small calculation, simply use a synchronous method:

public int CalculateTotal()
{
int total = 0;
    for (int i = 0; i < 1_000_000; i++)
{
total += i;
}
return total;
}

For expensive CPU-bound work, consider a suitable background-processing strategy or parallel computation when justified. Moving CPU-bound work to Task.Run isn't automatically beneficial in ASP.NET Core and can consume additional thread-pool resources.

Remember: async is particularly useful for I/O-bound operations. It doesn't magically make CPU-intensive work execute faster.

❌ Mistake 5: Ignoring cancellation in long-running operations

Imagine a customer starts a report download and then closes the browser.

If the operation supports cancellation but your code ignores the cancellation signal, it may continue doing unnecessary work.

The wrong approach

public async Task<IActionResult> GetReport()
{
var report = await _reportService
.GenerateReportAsync();
    return Ok(report);
}

✅ The better approach

ASP.NET Core exposes a cancellation token through HttpContext.RequestAborted.

public async Task<IActionResult> GetReport(
CancellationToken cancellationToken)
{
var report = await _reportService
.GenerateReportAsync(cancellationToken);
    return Ok(report);
}

Your service should accept and pass the token to operations that support cancellation.

public async Task<Report> GenerateReportAsync(
CancellationToken cancellationToken)
{
return await _repository
.GetReportAsync(cancellationToken);
}

Cancellation is cooperative: the underlying operation must observe and honor the token. It won’t automatically stop arbitrary code or undo work already completed.

Remember: Propagate cancellation tokens through long-running database, HTTP, and file operations whenever supported.

🎯 Bonus: A few async/await best practices

  • Use Task<T> when an asynchronous method returns a value.
  • Prefer Task over async void for regular methods.
  • Avoid blocking asynchronous operations with .Result and .Wait().
  • Use Task.WhenAll when independent operations can safely run concurrently.
  • Pass cancellation tokens to operations that support cancellation.
  • Don’t add async to a method unless it needs asynchronous behavior.
  • Use ConfigureAwait(false) when appropriate in reusable libraries; it is not a universal requirement in ASP.NET Core application code.
  • Avoid fire-and-forget tasks for important business operations unless their lifecycle, error handling, and reliability are explicitly managed.

💡 Final takeaway

Writing asynchronous code isn’t just about adding async and await everywhere.

It’s about choosing the right execution model, avoiding unnecessary blocking, handling failures correctly, and making efficient use of server resources.

Small improvements in asynchronous programming can make a meaningful difference in the scalability and maintainability of your .NET applications.

Question for developers: Which async/await mistake have you encountered most often in real-world projects?

Share your experience in the comments!

♻️ Save this post for your next .NET project, and share it with a fellow developer.

#dotnet #csharp #aspnetcore #asyncawait #webdevelopment #softwareengineering #programming

--

--