Skip to content

Understanding await in C#

Overview

await is part of C#’s asynchronous programming model, introduced in C# 5.0. It allows you to pause the execution of a method until an asynchronous operation (a Task or Task) completes — without blocking the current thread.

In other words:

"await lets your code run asynchronously, but look and behave as if it’s synchronous."

How It Works (Step by Step)

When you write something like:

var data = await GetDataAsync();

Here’s what happens under the hood:

  • GetDataAsync() starts running and returns a Task.
  • The await keyword checks if the task has completed:
  • If it has completed, execution continues immediately.
  • If it has not completed, the method:
    • Registers a continuation (the rest of your method) to run when the task finishes.
    • Returns control to the caller without blocking the current thread.
  • Once the awaited task completes, the method resumes execution from where it left off.

Example: Sequential vs Asynchronous Flow

Synchronous Example

public string GetData()
{
    Thread.Sleep(3000); // Blocks the thread for 3 seconds
    return "Finished!";
}

The thread doing this work is stuck — nothing else can use it.

Bad for scalability (especially in servers handling many requests).

Asynchronous Example

public async Task<string> GetDataAsync()
{
    await Task.Delay(3000); // Non-blocking 3s delay
    return "Finished!";
}

The await pauses the method logically, but the thread is released.

While waiting, the thread can handle other requests.

When the delay finishes, the runtime resumes execution where it left off.

Program Flow Illustration

Without await

Main Thread → [Task Starts] → Waits (blocked) → Continues

With await

Main Thread → [Task Starts] → Returns to caller (free) → Task Completes → [Continuation resumes]

So, await frees up threads while still maintaining a logical, readable sequence of operations.

Performance Considerations

Aspect Explanation
Non-blocking await releases threads instead of blocking them — crucial for scalability in web apps.
Thread efficiency Fewer threads are needed to handle more concurrent operations.
Responsiveness Ideal for UI apps — UI remains responsive while awaiting background work.
Context capture By default, await captures the current synchronization context (e.g., UI thread). You can opt out using ConfigureAwait(false) for library or server code.

Example

await SomeAsyncOperation().ConfigureAwait(false);

This tells the runtime:

“I don’t care about resuming on the original context — just continue on any thread.”

This improves performance and avoids deadlocks in ASP.NET or library code.

When You Should Use await

Use await when:

  • Calling I/O-bound asynchronous operations, e.g.:
    • HTTP requests (HttpClient.GetAsync)
    • File or network I/O (FileStream.ReadAsync)
    • Database calls (SqlCommand.ExecuteReaderAsync)
  • You want to avoid blocking threads (especially in web or UI apps).
  • You want to compose async operations cleanly.

Example

var response = await httpClient.GetAsync("https://api.github.com");
var content = await response.Content.ReadAsStringAsync();

When You Should NOT Use await

Don’t use await for:

  • CPU-bound work — use Task.Run for that instead:
var result = await Task.Run(() => DoCpuHeavyCalculation());
  • Short synchronous operations that don’t involve I/O — async overhead may outweigh benefits.
  • Inside tight loops with many small async calls — prefer batching or parallel async patterns.

Common Mistakes & Best Practices

Mistake Fix / Best Practice
Blocking on async (.Result, .Wait()) Always use await all the way up the call stack
Forgetting async keyword on method Required to use await
Async method returns void Avoid this — use Task instead, unless it’s an event handler
Ignoring ConfigureAwait(false) in library/server code Use it to improve performance and avoid deadlocks
Async without real async work Don’t use async/await unless an actual async operation is being awaited

Speeding up sequential tasks

As with other time consuming tasks, we can speed things up by using Task.WhenAll(). Suppose we have the following code:

var customers = await Service.GetCustomers(); // Takes at least 15 seconds
var locations = await Service.GetLocations(); // Takes at least 10 seconds

This would take 25 second to complete (and move on to any following code). By re-writing this as:

var customersTask = Service.GetCustomers();
var locationsTask = Service.GetLocations();

await Task.WhenAll(customersTask, locationsTask);

var customers = customersTask.Result;
var locations = locationsTask.Result;

The code only takes as long as the longest operation takes to complete, in this case about 15 seconds, before being able to continue.

Summary

In C#, await is used to asynchronously wait for a Task to complete without blocking the current thread. It effectively ‘pauses’ a method while releasing the thread to do other work, and then resumes execution when the awaited operation finishes.

This is ideal for I/O-bound operations like API calls or file access, as it improves scalability and responsiveness. However, await isn’t beneficial for CPU-bound work, where you’d use Task.Run instead.

In server-side code combine await with ConfigureAwait(false) to avoid context switching overhead and potential deadlocks.