.NET Core  

How to Use Async and Await Correctly in .NET Applications

Introduction

Asynchronous programming is a powerful feature in .NET that helps applications run faster and stay responsive. Whether you're building a web API, desktop app, mobile app, or background service, using async and await correctly is essential for good performance. However, many developers use these keywords without fully understanding how they work, which can lead to unexpected delays, deadlocks, or blocked threads.

This article explains async and await in simple words, shows how they work behind the scenes, covers best practices, and includes clear examples you can use in real .NET applications.

What is Asynchronous Programming in .NET?

Asynchronous programming allows your application to continue running while waiting for long-running tasks—such as API calls, database queries, file IO, or network operations—to complete.

Synchronous (Blocking) Example

var data = DownloadFile(); // Blocks the thread
Console.WriteLine("Done");

The thread waits (blocks) until DownloadFile completes.

Asynchronous (Non-blocking) Example

var data = await DownloadFileAsync();
Console.WriteLine("Done");

The thread does not block and can do other work while waiting.

Understanding async and await

1. async keyword

  • Used on methods.

  • Allows using the await keyword inside.

  • Converts the method into an asynchronous method.

2. await keyword

  • Pauses the method.

  • Frees the thread to do other work.

  • Resumes execution when the awaited task completes.

Example

public async Task<string> GetMessageAsync()
{
    await Task.Delay(2000); // simulate time-consuming work
    return "Hello from async!";
}

Why async and await Improve Performance

  • They do not create new threads.

  • They free the current thread until work is done.

  • They allow the server or UI to stay responsive.

  • They help scale web APIs to handle more requests.

How to Use async and await in Real .NET Applications

1. Asynchronous Methods Should Return Task or Task

Correct

public async Task SaveDataAsync() { }
public async Task<string> LoadDataAsync() { return "data"; }

Incorrect

public async void SaveData() { } // Avoid async void!

Why avoid async void?

  • Exceptions cannot be caught.

  • Difficult to test.

  • Only safe for event handlers:

private async void Button_Click(object sender, EventArgs e)
{
    await LoadDataAsync();
}

2. Always await asynchronous calls

If you forget to use await, the method becomes fire-and-forget, leading to bugs.

Incorrect

SaveDataAsync(); // not awaited
Console.WriteLine("Done");

Correct

await SaveDataAsync();
Console.WriteLine("Done");

3. Use async all the way down

If your top-level method is async, everything it calls should also be async.

Anti-pattern

public void Process()
{
    var result = GetDataAsync().Result; // Deadlock risk
}

Correct

public async Task ProcessAsync()
{
    var result = await GetDataAsync();
}

4. Avoid .Result and .Wait()

These block the thread and can cause deadlocks, especially in ASP.NET and UI apps.

Example of problem

var data = GetDataAsync().Result; // blocks

Fix

var data = await GetDataAsync();

5. Use ConfigureAwait(false) in library code

This prevents capturing the original context unnecessarily.

Example

await Task.Delay(1000).ConfigureAwait(false);

Use this when writing:

  • Class libraries

  • SDKs

  • Background services

Do NOT use it in ASP.NET or UI where context matters.

6. Combine multiple async tasks with Task.WhenAll

When tasks can run in parallel, use Task.WhenAll.

Example

var t1 = GetUserAsync();
var t2 = GetOrdersAsync();
var t3 = GetPaymentsAsync();

await Task.WhenAll(t1, t2, t3);

Console.WriteLine(t1.Result);

This improves speed by running tasks simultaneously.

7. Do NOT wrap synchronous code in Task.Run unnecessarily

Bad example:

await Task.Run(() => File.ReadAllText("data.txt"));

Use actual async method:

await File.ReadAllTextAsync("data.txt");

Task.Run is only useful for CPU-bound tasks.

8. Async for CPU-bound vs I/O-bound tasks

I/O-bound example (network, file, DB)

Use async methods:

await httpClient.GetStringAsync(url);

CPU-bound example (calculation)

Use Task.Run:

await Task.Run(() => HeavyCalculation());

9. Handle exceptions in async methods

Use try/catch:

try
{
    var data = await LoadDataAsync();
}
catch(Exception ex)
{
    Console.WriteLine(ex.Message);
}

Exceptions flow automatically through async tasks when awaited.

10. Use CancellationToken for long-running async tasks

Example

public async Task DownloadAsync(CancellationToken token)
{
    for (int i = 0; i < 10; i++)
    {
        token.ThrowIfCancellationRequested();
        await Task.Delay(500);
    }
}

This improves performance and user experience.

Full Example of Async Await in a .NET API

[HttpGet("/products")]
public async Task<IActionResult> GetProducts()
{
    var list = await _service.GetProductsAsync();
    return Ok(list);
}

Here:

  • The request does not block

  • Server can serve other users at the same time

  • API becomes more scalable

Best Practices Summary

  • Use async/await for I/O tasks

  • Avoid async void except for events

  • Avoid blocking calls: .Wait(), .Result

  • Use Task.WhenAll for parallel asynchronous work

  • Propagate async through method calls

  • Handle cancellation properly

Conclusion

Using async and await correctly is one of the most important skills for .NET developers. These keywords make your applications faster, more responsive, and more scalable, especially when dealing with network calls, file IO, or database operations. By following the best practices in this guide—such as avoiding blocking calls, using Task-based patterns, and writing async all the way down—you can prevent common mistakes and build reliable, high-performance .NET applications.