C# Async and Await Explained: Write Responsive Asynchronous Code

Asynchronous programming allows C# applications to perform operations without unnecessarily blocking the current thread. The async and await keywords make asynchronous code easier to write, read, and maintain.

What is Asynchronous Programming?

Asynchronous programming is a programming approach where an application can start a long-running operation and continue doing other useful work while waiting for that operation to complete.

CSHARP
static async Task DownloadDataAsync()
{
    await Task.Delay(2000);
    Console.WriteLine("Data downloaded");
}

await DownloadDataAsync();

Why Use async and await?

  • Avoid blocking threads during asynchronous operations
  • Improve application responsiveness
  • Handle network and file operations efficiently
  • Write asynchronous code in a readable style
  • Improve scalability in server applications
  • Simplify asynchronous exception handling

What is a Task?

Task represents an asynchronous operation. A Task can represent an operation that will complete later, while Task represents an asynchronous operation that eventually produces a value.

CSHARP
static async Task<int> GetNumberAsync()
{
    await Task.Delay(1000);
    return 42;
}

int result = await GetNumberAsync();

Console.WriteLine(result);

Using the await Keyword

The await keyword asynchronously waits for a Task to complete. It allows the method to pause at that point without blocking the thread while the awaited operation is in progress.

CSHARP
static async Task<string> GetMessageAsync()
{
    await Task.Delay(1000);
    return "Hello from async code";
}

string message = await GetMessageAsync();

Console.WriteLine(message);

Async Method Naming

A common C# convention is to add the Async suffix to methods that return a Task or Task and perform asynchronous work.

CSHARP
static async Task SaveDataAsync()
{
    await Task.Delay(500);
}

static async Task<string> LoadDataAsync()
{
    await Task.Delay(500);
    return "Data";
}

Returning Values with Task

When an asynchronous method needs to return a value, use Task. The result becomes available after the task completes.

CSHARP
static async Task<int> CalculateAsync()
{
    await Task.Delay(500);
    return 100 + 50;
}

int result = await CalculateAsync();

Console.WriteLine(result);

Calling Multiple Async Operations

When multiple independent asynchronous operations can run at the same time, their tasks can be started first and awaited together.

CSHARP
Task<string> firstTask = GetDataAsync("First");
Task<string> secondTask = GetDataAsync("Second");

string first = await firstTask;
string second = await secondTask;

Console.WriteLine(first);
Console.WriteLine(second);

static async Task<string> GetDataAsync(string name)
{
    await Task.Delay(1000);
    return $"{name} completed";
}

Using Task.WhenAll

Task.WhenAll is useful when several asynchronous operations should complete before the program continues.

CSHARP
Task<string> firstTask = GetDataAsync("First");
Task<string> secondTask = GetDataAsync("Second");
Task<string> thirdTask = GetDataAsync("Third");

string[] results = await Task.WhenAll(
    firstTask,
    secondTask,
    thirdTask
);

foreach (string result in results)
{
    Console.WriteLine(result);
}

Async Exception Handling

Exceptions from awaited asynchronous operations can be handled using normal try-catch blocks.

CSHARP
try
{
    await ProcessDataAsync();
}
catch (Exception ex)
{
    Console.WriteLine($"Error: {ex.Message}");
}

static async Task ProcessDataAsync()
{
    await Task.Delay(500);
    throw new InvalidOperationException("Processing failed");
}

Async File Operations

The .NET libraries provide asynchronous APIs for many I/O operations, including file access. These APIs are useful when applications need to avoid blocking while waiting for storage operations.

CSHARP
string content = await File.ReadAllTextAsync("data.txt");

Console.WriteLine(content);

Async HTTP Requests

HttpClient provides asynchronous methods for network requests. This is especially useful for applications that communicate with external APIs.

CSHARP
using HttpClient client = new HttpClient();

string response = await client.GetStringAsync(
    "https://example.com"
);

Console.WriteLine(response);

Cancellation with CancellationToken

CancellationToken allows an application to request that an asynchronous operation stop before completion. This is useful for long-running operations and user-cancelled tasks.

CSHARP
static async Task ProcessAsync(CancellationToken token)
{
    for (int i = 0; i < 10; i++)
    {
        token.ThrowIfCancellationRequested();
        await Task.Delay(500, token);
        Console.WriteLine(i);
    }
}

using CancellationTokenSource cts = new();

await ProcessAsync(cts.Token);

Async vs Synchronous Code

FeatureSynchronousAsynchronous
ExecutionWaits for operation to finishCan continue while operation is pending
Thread BlockingMay block during I/OTypically avoids blocking during awaited I/O
SyntaxNormal method callsasync and await
Common UseSimple CPU-bound operationsI/O-bound operations

Async Does Not Automatically Mean Parallel

Asynchronous programming and parallel programming are different concepts. Async code is primarily useful for operations that spend time waiting, while parallelism is about performing multiple units of work at the same time.

CSHARP
Task task1 = Task.Delay(1000);
Task task2 = Task.Delay(1000);

await Task.WhenAll(task1, task2);

Console.WriteLine("Both completed");

Common async and await Mistakes

  • Using .Result or .Wait() unnecessarily
  • Forgetting to await an asynchronous operation
  • Using async for work that does not benefit from asynchronous execution
  • Starting independent operations sequentially when they could run concurrently
  • Ignoring cancellation for long-running operations
  • Creating unnecessary async wrappers around synchronous code

Async Programming Best Practices

  • Prefer await over blocking with Result or Wait
  • Use asynchronous APIs for I/O-bound operations
  • Propagate async all the way through the call chain when appropriate
  • Use CancellationToken for operations that may need cancellation
  • Use Task.WhenAll for independent asynchronous operations
  • Handle expected exceptions at appropriate application boundaries

Real-World Applications

  • Calling REST APIs
  • Reading and writing files
  • Database operations
  • Cloud service requests
  • Background data processing
  • ASP.NET Core web applications
  • Network communication

Practice Exercises

  • Create a method that returns Task
  • Use await with Task.Delay
  • Read a file asynchronously
  • Call an HTTP endpoint asynchronously
  • Run multiple independent tasks with Task.WhenAll
  • Handle an asynchronous exception
  • Add CancellationToken support to a long-running operation

Conclusion

The async and await features make asynchronous programming much easier to understand and maintain in C#. They are especially valuable for I/O-bound operations such as network requests, file access, and database communication, where avoiding unnecessary thread blocking can improve application responsiveness and scalability.

Note: Note: async and await are not automatically a performance boost for every piece of code. They are most useful when an operation spends time waiting, especially on I/O.