Skip to content

When to Use and When Not to Use async and await

This is the last lesson of the series. We turn everything we learned into simple, practical rules you can apply every day.

One-line rule: use async/await when your code waits for something outside the program. Don't use it when your code only calculates and finishes quickly.

When to use async and await

1. Database calls

var customer = await _context.Customers.FirstOrDefaultAsync(c => c.Id == id);
await _context.SaveChangesAsync();

await connection.OpenAsync();
int rows = await command.ExecuteNonQueryAsync();

2. Calling web APIs and other services

HttpResponseMessage response = await _httpClient.GetAsync("api/orders");
string json = await response.Content.ReadAsStringAsync();

3. Reading and writing files

string text = await File.ReadAllTextAsync(path);
await File.WriteAllTextAsync(path, text);

4. ASP.NET Core controllers, Razor Pages and middleware

Any action that touches a database, cache, file or another service should be async Task<IActionResult>. This keeps the server scalable.

5. Desktop event handlers that do slow work

private async void btnSave_Click(object sender, EventArgs e)
{
    btnSave.Enabled = false;
    await _service.SaveAsync(_order);
    btnSave.Enabled = true;
}

6. Running several independent operations together

await Task.WhenAll(SendEmailAsync(user), SendSmsAsync(user), WriteAuditLogAsync(user));

7. Whenever a library gives you an Async method

If you are already inside an async method and the library offers both Save() and SaveAsync(), prefer await SaveAsync().

When NOT to use async and await

1. Quick, in-memory, CPU-only code

// Don't do this
public async Task<int> AddAsync(int a, int b)
{
    return await Task.FromResult(a + b);
}

// Do this
public int Add(int a, int b) => a + b;

Calculations, string formatting, mapping objects and reading from a Dictionary or a List have nothing to wait for. Keep them synchronous.

2. "Fake async": wrapping synchronous code in Task.Run on the server

// Not useful in ASP.NET Core: it still blocks a thread-pool thread,
// and adds the cost of switching threads
public Task<string> ReadFileAsync(string path)
{
    return Task.Run(() => File.ReadAllText(path));
}

// Better: use the real async API
public Task<string> ReadFileAsync(string path)
{
    return File.ReadAllTextAsync(path);
}

In ASP.NET Core, don't use Task.Run to "make code async". The request is already on a thread-pool thread, so you just swap one busy thread for another. Task.Run is useful in desktop apps, to move heavy CPU work off the UI thread.

3. Heavy CPU work that you want to finish faster

Async will not speed up a calculation. If you have a lot of CPU work, use Parallel.For, Parallel.ForEach or PLINQ instead (see Concurrency vs Parallelism).

4. Places where C# doesn't allow it

  • Constructors: use a static factory method, for example static async Task<ReportService> CreateAsync().
  • Properties: use a method, for example GetTotalAsync().
  • Inside a lock block: use SemaphoreSlim with await semaphore.WaitAsync().

5. async void anywhere except event handlers

Use async Task instead, so callers can await the method and catch its exceptions.

6. When you would have to block on it anyway

If you are stuck in code that can't be async (an old interface you can't change), calling an async method and then using .Result risks deadlocks. In that case, prefer the library's synchronous method if it has one, until you can make the whole chain async.

Quick decision checklist

QuestionIf yes
Does the code wait for a database, file, network or API?Use async/await with the library's Async method.
Is it an ASP.NET Core action that does any I/O?Make it async Task<IActionResult>.
Is it heavy CPU work in a desktop app?await Task.Run(...) to keep the UI responsive.
Is it heavy CPU work on a server or in a batch job?Keep it synchronous, or use Parallel to use more cores.
Is it quick, in-memory code?Keep it synchronous.
Do several independent I/O calls run one after another?Start them together and use Task.WhenAll.

Best practices to remember

  • Async all the way: from the database call up to the controller or event handler.
  • Don't block: never use .Result, .Wait() or GetAwaiter().GetResult() on async code if you can await it.
  • Name it right: async methods end with Async.
  • Return Task, not void: async void only for event handlers.
  • Pass a CancellationToken to long-running operations and accept one in your own async methods.
  • Run independent work together with Task.WhenAll.
  • Use ConfigureAwait(false) in reusable library code that doesn't touch the UI.
  • Always await your tasks, so exceptions are not lost.

What you learned in this series

  1. Synchronous programming runs one task at a time and waits for each one.
  2. Asynchronous programming starts work and continues without waiting.
  3. Blocking wastes the thread; non-blocking frees it.
  4. Concurrency deals with many tasks at once; parallelism does many tasks at once.
  5. async prepares a method; await pauses the method, not the thread.
  6. The compiler builds a state machine, and during real I/O no thread is waiting.
  7. Use async for waiting work, keep quick calculations synchronous, and use parallelism for heavy CPU work.

Summary

  • Use async/await for I/O: databases, files, web APIs, network, and slow UI actions.
  • Don't use it for quick in-memory code or to "fake" async with Task.Run on the server.
  • For heavy CPU work use Parallel (or Task.Run in desktop apps).
  • Constructors, properties and lock blocks can't use await.
  • Go async all the way, don't block, and always await your tasks.