What is the purpose of async and await in C#?

Understanding Asynchronous Programming

Asynchronous programming allows operations to run without blocking the main thread, improving application responsiveness and resource utilization. The async and await keywords in C# make asynchronous programming simpler and more intuitive.

// Synchronous method (blocks the thread)
public string DownloadWebPage(string url)
{
    using (var client = new WebClient())
    {
        return client.DownloadString(url); // Thread is blocked until download completes
    }
}

// Asynchronous method (doesn't block the thread)
public async Task<string> DownloadWebPageAsync(string url)
{
    using (var client = new HttpClient())
    {
        return await client.GetStringAsync(url); // Thread is released during download
    }
}

The Problem Async/Await Solves

Before async/await, asynchronous programming was complex, involving callbacks, manual state machines, or the Task Parallel Library (TPL) with continuation methods.

// Old way: Using Task continuations
public Task<string> GetDataAsync(string url)
{
    return httpClient.GetStringAsync(url)
        .ContinueWith(task =>
        {
            if (task.IsFaulted)
                return "Error: " + task.Exception.InnerException.Message;
                
            var data = task.Result;
            return ProcessData(data);
        });
}

// New way: Using async/await
public async Task<string> GetDataAsync(string url)
{
    try
    {
        string data = await httpClient.GetStringAsync(url);
        return ProcessData(data);
    }
    catch (Exception ex)
    {
        return "Error: " + ex.Message;
    }
}

How Async and Await Work

The async Keyword

The async modifier indicates that a method, lambda expression, or anonymous method is asynchronous and may contain await expressions.

// Async method declaration
public async Task<int> CalculateAsync()
{
    // Method body
}

// Async lambda expression
Func<int, Task<int>> calculate = async x => {
    await Task.Delay(100);
    return x * 2;
};

// Async anonymous method
button.Click += async (sender, e) => {
    await Task.Delay(100);
    label.Text = "Clicked";
};

The await Keyword

The await operator suspends execution of the method until the awaited task completes, without blocking the thread.

public async Task ProcessDataAsync()
{
    // Start the operation
    Task<string> dataTask = FetchDataAsync();
    
    // Do other work while the operation is in progress
    PrepareForData();
    
    // Suspend execution until the operation completes
    string data = await dataTask;
    
    // Continue execution after the operation completes
    ProcessResult(data);
}

Return Types for Async Methods

Async methods can return:

  • Task<T> for methods that return a value
  • Task for methods that don’t return a value
  • void for event handlers (not recommended for other scenarios)
  • ValueTask<T> or ValueTask for performance optimization (C# 7.0+)
// Returns a value
public async Task<string> GetNameAsync()
{
    await Task.Delay(100);
    return "John";
}

// Doesn't return a value
public async Task UpdateDatabaseAsync()
{
    await Task.Delay(100);
    // Update database
}

// Event handler (avoid for other scenarios)
public async void Button_Click(object sender, EventArgs e)
{
    await Task.Delay(100);
    label.Text = "Clicked";
}

// ValueTask for performance optimization
public async ValueTask<int> GetCachedValueAsync()
{
    if (_cache.TryGetValue("key", out int value))
        return value; // No Task allocation needed
        
    value = await ComputeValueAsync();
    _cache["key"] = value;
    return value;
}

Asynchronous Patterns

Sequential Execution

// Execute tasks one after another
public async Task SequentialExecutionAsync()
{
    var result1 = await Task1Async();
    var result2 = await Task2Async(result1);
    var result3 = await Task3Async(result2);
    return result3;
}

Parallel Execution

// Execute tasks in parallel
public async Task ParallelExecutionAsync()
{
    Task<int> task1 = Task1Async();
    Task<int> task2 = Task2Async();
    Task<int> task3 = Task3Async();
    
    // Wait for all tasks to complete
    await Task.WhenAll(task1, task2, task3);
    
    // Use the results
    int sum = task1.Result + task2.Result + task3.Result;
    return sum;
}

First Completed Task

// Use the first task that completes
public async Task<string> GetFastestResponseAsync()
{
    Task<string> task1 = Service1Async();
    Task<string> task2 = Service2Async();
    
    // Wait for the first task to complete
    Task<string> completedTask = await Task.WhenAny(task1, task2);
    
    // Get the result from the completed task
    return await completedTask;
}

Exception Handling in Async Methods

Exceptions in async methods are captured and placed on the returned Task.

// Exception handling in async methods
public async Task ExceptionHandlingAsync()
{
    try
    {
        // This might throw an exception
        string data = await FetchDataAsync();
        ProcessData(data);
    }
    catch (HttpRequestException ex)
    {
        // Handle network error
        LogError("Network error", ex);
    }
    catch (Exception ex)
    {
        // Handle other errors
        LogError("General error", ex);
    }
    finally
    {
        // Clean up resources
        CleanUp();
    }
}

Aggregated Exceptions

When using Task.WhenAll, multiple exceptions are aggregated into an AggregateException.

// Handling multiple exceptions
public async Task HandleMultipleExceptionsAsync()
{
    var tasks = new List<Task>();
    tasks.Add(Task1Async());
    tasks.Add(Task2Async());
    
    try
    {
        await Task.WhenAll(tasks);
    }
    catch (Exception ex)
    {
        // ex is the first exception that was thrown
        
        // To get all exceptions:
        if (tasks[0].IsFaulted) Console.WriteLine(tasks[0].Exception);
        if (tasks[1].IsFaulted) Console.WriteLine(tasks[1].Exception);
        
        // Or use Task.WhenAll result directly:
        try
        {
            Task.WhenAll(tasks).Wait();
        }
        catch (AggregateException ae)
        {
            foreach (var innerEx in ae.InnerExceptions)
            {
                Console.WriteLine(innerEx);
            }
        }
    }
}

Cancellation Support

Async operations often support cancellation using CancellationToken.

// Cancellable async method
public async Task<string> DownloadAsync(string url, CancellationToken cancellationToken)
{
    using (var client = new HttpClient())
    {
        // Pass the token to the async operation
        return await client.GetStringAsync(url, cancellationToken);
    }
}

// Using cancellation
public async Task ProcessWithCancellationAsync()
{
    // Create a cancellation token source with timeout
    using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)))
    {
        try
        {
            string result = await DownloadAsync("https://example.com", cts.Token);
            ProcessResult(result);
        }
        catch (OperationCanceledException)
        {
            Console.WriteLine("Operation was cancelled or timed out");
        }
    }
}

Progress Reporting

Async methods can report progress using IProgress<T> and Progress<T>.

// Async method with progress reporting
public async Task<byte[]> DownloadFileAsync(string url, IProgress<int> progress)
{
    using (var client = new HttpClient())
    {
        // Get the total file size
        var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
        var totalBytes = response.Content.Headers.ContentLength ?? -1L;
        
        using (var stream = await response.Content.ReadAsStreamAsync())
        {
            var buffer = new byte[8192];
            var totalBytesRead = 0L;
            var bytesRead = 0;
            var data = new MemoryStream();
            
            while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length)) > 0)
            {
                await data.WriteAsync(buffer, 0, bytesRead);
                totalBytesRead += bytesRead;
                
                if (totalBytes > 0 && progress != null)
                {
                    var progressPercentage = (int)((totalBytesRead * 100) / totalBytes);
                    progress.Report(progressPercentage);
                }
            }
            
            return data.ToArray();
        }
    }
}

// Using progress reporting
public async Task DownloadWithProgressAsync()
{
    var progress = new Progress<int>(percent => {
        progressBar.Value = percent;
        statusLabel.Text = $"Downloading: {percent}%";
    });
    
    try
    {
        byte[] data = await DownloadFileAsync("https://example.com/largefile.zip", progress);
        File.WriteAllBytes("largefile.zip", data);
        statusLabel.Text = "Download complete!";
    }
    catch (Exception ex)
    {
        statusLabel.Text = $"Error: {ex.Message}";
    }
}

Common Async Pitfalls

1. Async Void

Avoid async void except for event handlers, as exceptions can’t be caught and the caller can’t await completion.

// Bad: async void
public async void BadMethod() // Can't be awaited, exceptions are unhandled
{
    await Task.Delay(100);
    throw new Exception("This exception is lost!");
}

// Good: async Task
public async Task GoodMethod() // Can be awaited, exceptions are propagated
{
    await Task.Delay(100);
    throw new Exception("This exception can be caught!");
}

2. Blocking on Async Code

Never block on async code using .Wait(), .Result, or .GetAwaiter().GetResult() in synchronous contexts, as it can cause deadlocks.

// Bad: Blocking on async code
public void BadMethod()
{
    // This can deadlock in UI or ASP.NET contexts
    var result = GetDataAsync().Result;
    ProcessData(result);
}

// Good: Keep the async chain
public async Task GoodMethod()
{
    var result = await GetDataAsync();
    ProcessData(result);
}

3. Forgetting to Await

Forgetting to await an async method means the task runs in the background without waiting for completion.

// Bad: Missing await
public async Task BadMethod()
{
    SaveDataAsync(); // Fire and forget, not awaited
    return; // Method returns before SaveDataAsync completes
}

// Good: Using await
public async Task GoodMethod()
{
    await SaveDataAsync(); // Wait for completion
    return; // Method returns after SaveDataAsync completes
}

4. Unnecessary Async/Await

Don’t use async/await when not needed, as it adds overhead.

// Unnecessary async/await
public async Task<int> UnnecessaryAsync()
{
    return await Task.FromResult(42); // Unnecessary await
}

// Better
public Task<int> Better()
{
    return Task.FromResult(42); // Directly return the task
}

Async/Await in ASP.NET Core

Async/await is particularly valuable in web applications for handling concurrent requests efficiently.

// ASP.NET Core controller with async methods
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IProductRepository _repository;
    
    public ProductsController(IProductRepository repository)
    {
        _repository = repository;
    }
    
    [HttpGet]
    public async Task<ActionResult<IEnumerable<Product>>> GetProducts()
    {
        var products = await _repository.GetAllAsync();
        return Ok(products);
    }
    
    [HttpGet("{id}")]
    public async Task<ActionResult<Product>> GetProduct(int id)
    {
        var product = await _repository.GetByIdAsync(id);
        
        if (product == null)
            return NotFound();
            
        return Ok(product);
    }
    
    [HttpPost]
    public async Task<ActionResult<Product>> CreateProduct(Product product)
    {
        await _repository.AddAsync(product);
        return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, product);
    }
}

Interview Tips

  1. Define clearly: Async/await is a language feature that simplifies asynchronous programming by allowing you to write asynchronous code that looks like synchronous code.

  2. Explain the benefits: Highlight improved responsiveness, scalability, and resource utilization without the complexity of callbacks or manual state machines.

  3. Thread behavior: Emphasize that await doesn’t block the thread but releases it back to the thread pool until the awaited task completes.

  4. Return types: Know the appropriate return types for async methods (Task, Task<T>, ValueTask, ValueTask<T>).

  5. Common patterns: Discuss sequential execution, parallel execution, and exception handling in async code.

  6. Pitfalls: Be prepared to explain common mistakes like async void, blocking on async code, and forgetting to await.

  7. Real-world examples: Provide examples of when async/await is particularly valuable, such as I/O operations, network requests, and database access.

Test Your Knowledge

Take a quick quiz to test your understanding of this topic.

Quiz: Async Await

Loading quiz...