C# Async/Await

Asynchronous programming made simple

⏳ What is Async/Await?

Async/Await enables asynchronous programming, allowing your application to perform long-running operations without blocking the main thread. This keeps your UI responsive and improves application performance significantly.


// Simple async method
async Task<string> GetDataAsync()
{
    await Task.Delay(1000); // Simulate delay
    return "Data loaded!";
}
                                    

Output:

Data loaded!

Key Async Concepts

πŸ”„

async Keyword

Marks a method as asynchronous

async Task MyMethodAsync()
⏸️

await Keyword

Waits for async operation to complete

await Task.Delay(1000);
πŸ“¦

Task Return Type

Represents asynchronous operation

Task<int> GetNumberAsync()
⚑

Non-Blocking

Keeps application responsive

// UI stays responsive

πŸ”Ή Basic Async Method

An async method in C# is declared using the async keyword and typically returns a Task or Task<T>. The await keyword pauses execution until the asynchronous operation completes, preventing thread blocking and maintaining application responsiveness during I/O-bound or long-running tasks. This pattern is fundamental for scalable applications, allowing efficient resource use while waiting for operations like file reads, network requests, or database queries. It keeps your UI smooth and servers responsive under load.

using System;
using System.Threading.Tasks;

class Program
{
    // Async method that returns a Task
    static async Task GreetAsync()
    {
        Console.WriteLine("Starting...");
        await Task.Delay(2000); // Wait 2 seconds
        Console.WriteLine("Hello after 2 seconds!");
    }

    static async Task Main()
    {
        await GreetAsync();
        Console.WriteLine("Done!");
    }
}

Output:

Starting...

(2 second delay)

Hello after 2 seconds!

Done!

πŸ”Ή Async Method with Return Value

Async methods can return values by using the generic Task<T> type, where T is the result type. The await keyword unwraps the final value from the completed task. This is essential for operations that need to produce a result, such as fetching data from an API, reading a configuration file, or querying a database. Returning Task<int>, Task<string>, or other types makes async code both non-blocking and result-oriented, seamlessly integrating asynchronous workflows into your application's logic.

using System;
using System.Threading.Tasks;

class Program
{
    // Async method returning a value
    static async Task<int> CalculateAsync(int x, int y)
    {
        Console.WriteLine("Calculating...");
        await Task.Delay(1000); // Simulate work
        return x + y;
    }

    static async Task Main()
    {
        int result = await CalculateAsync(5, 10);
        Console.WriteLine($"Result: {result}");
    }
}

Output:

Calculating...

(1 second delay)

Result: 15

πŸ”Ή Multiple Async Operations

You can execute multiple async operations either sequentially or in parallel, depending on your performance needs. Sequential execution uses await for each task, waiting for completion before starting the next. For independent operations, Task.WhenAll runs them concurrently, dramatically reducing total execution time. This is ideal for calling multiple web services, performing simultaneous database reads, or processing independent data streams. Properly managing concurrency is key to building high-performance, responsive applications that maximize throughput.

using System;
using System.Threading.Tasks;

class Program
{
    static async Task<string> FetchData1Async()
    {
        await Task.Delay(1000);
        return "Data 1";
    }

    static async Task<string> FetchData2Async()
    {
        await Task.Delay(1000);
        return "Data 2";
    }

    static async Task Main()
    {
        // Sequential (takes 2 seconds)
        string data1 = await FetchData1Async();
        string data2 = await FetchData2Async();
        Console.WriteLine($"{data1}, {data2}");

        // Parallel (takes 1 second)
        Task<string> task1 = FetchData1Async();
        Task<string> task2 = FetchData2Async();
        string[] results = await Task.WhenAll(task1, task2);
        Console.WriteLine($"{results[0]}, {results[1]}");
    }
}

Output:

Data 1, Data 2

Data 1, Data 2

πŸ”Ή Error Handling with Async

Exception handling in async methods uses standard try-catch blocks, similar to synchronous code. Exceptions thrown in an async method are captured within the returned Task and re-thrown when the task is awaited. This provides a consistent and intuitive error-handling model. You can catch specific exceptions like HttpRequestException for network calls or IOException for file operations, ensuring your application gracefully handles failures, logs errors, and provides fallback logic without crashing.

using System;
using System.Threading.Tasks;

class Program
{
    static async Task<int> DivideAsync(int a, int b)
    {
        await Task.Delay(500);
        if (b == 0)
            throw new DivideByZeroException("Cannot divide by zero!");
        return a / b;
    }

    static async Task Main()
    {
        try
        {
            int result = await DivideAsync(10, 2);
            Console.WriteLine($"Result: {result}");

            result = await DivideAsync(10, 0);
            Console.WriteLine($"Result: {result}");
        }
        catch (DivideByZeroException ex)
        {
            Console.WriteLine($"Error: {ex.Message}");
        }
    }
}

Output:

Result: 5

Error: Cannot divide by zero!

πŸ”Ή Async Best Practices

Follow established conventions to write robust and maintainable asynchronous code. Suffix async method names with "Async" (e.g., GetDataAsync). Avoid async void except for event handlers. Always await tasks to properly capture exceptions. In library code, use ConfigureAwait(false) to improve performance and prevent potential deadlocks by avoiding forced context marshaling. These practices ensure your async code is reliable, performant, and easier to debug and maintain across different execution contexts.

using System;
using System.Threading.Tasks;

class Program
{
    // βœ… Good: Async suffix, returns Task
    static async Task ProcessDataAsync()
    {
        await Task.Delay(1000);
        Console.WriteLine("Data processed");
    }

    // βœ… Good: Await the async method
    static async Task Main()
    {
        await ProcessDataAsync();
    }

    // ❌ Bad: Async void (only use for event handlers)
    // static async void ProcessData() { }

    // ❌ Bad: Not awaiting async method
    // ProcessDataAsync(); // Fire and forget - avoid this
}

Output:

Data processed

🧠 Test Your Knowledge

What keyword is used to wait for an async operation?