All posts
Written by Sujith Quintelier Sep 24, 2025

Mastering Asynchronous Programming with C# async/await - Part 2: Deep Dive

Deep dive into CSharp async/await: method anatomy, return types (Task, Task<T>, void), real meaning of await, and sequential vs parallel execution.
Sep 24, 2025

Part 2: Deep Dive into async and await

In Part 1, we saw why asynchronous programming matters and how async/await can make our apps more responsive. Now it’s time to go deeper into how it actually works.


Anatomy of an async Method

Let’s look at a simple method:

public async Task DoWorkAsync()
{
    Console.WriteLine("Step 1: Start work");

    await Task.Delay(2000);

    Console.WriteLine("Step 2: Work complete");
}

Breaking it down:

  • async → marks the method as asynchronous.
  • Task → return type that represents ongoing work.
  • await Task.Delay(2000) → pauses execution here until the delay finishes.

Important: The method doesn’t block the thread. The runtime pauses at the await, then comes back to resume execution when the task completes.


Return Types of Async Methods

There are three common return types for async methods:

  1. Task → when you don’t return a value.

    public async Task SaveAsync() { ... }
    
  2. Task<T> → when you return a value.

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

    Usage:

    int result = await GetNumberAsync();
    
  3. void → should be avoided (fire-and-forget), except for event handlers.

    private async void Button_Click(object sender, EventArgs e) 
    {
        await SaveAsync();
    }
    

    Problem: You can’t await an async void, making error handling tricky.


What Does await Really Do?

At first glance, await looks like “just wait here until it’s done.”
But that’s not quite right.

Here’s what actually happens:

  1. The method starts running.
  2. When it hits await someTask;, it checks if the task is already complete.
    • If yes → continues immediately.
    • If no → pauses execution.
  3. The compiler generates a state machine behind the scenes. It remembers:
    • Where the method left off
    • Local variables
    • What should happen when the task completes
  4. When the task finishes, the method resumes right after the await.

So await is really “register a continuation and resume later.”


Sequential vs. Asynchronous Execution

Here’s an example that shows the difference:

public async Task SequentialAsync()
{
    var client = new HttpClient();

    // Sequential (slower)
    var page1 = await client.GetStringAsync("https://example.com/page1");
    var page2 = await client.GetStringAsync("https://example.com/page2");

    Console.WriteLine("Sequential done");
}

This runs page1, waits, then runs page2.

Now let’s run them in parallel:

public async Task ParallelAsync()
{
    var client = new HttpClient();

    // Start both tasks immediately
    var task1 = client.GetStringAsync("https://example.com/page1");
    var task2 = client.GetStringAsync("https://example.com/page2");

    // Wait for both to complete
    await Task.WhenAll(task1, task2);

    Console.WriteLine("Parallel done");
}

Both requests are in-flight together → much faster.


Key Takeaways

  • async makes a method awaitable.
  • Return types matter: use Task or Task<T>, avoid void.
  • await doesn’t block — it sets up a continuation and resumes later.
  • Sequential awaits can be slow; sometimes it’s better to run tasks in parallel with Task.WhenAll.

👉 In Part 3, we’ll cover:

  • The dangers of async void
  • Deadlocks and ConfigureAwait(false)
  • Mixing sync and async code safely
  • Exception handling in async methods

Series Navigation

Previous: Part 1 – Introduction Series Index: Overview Next: Part 3 – Pitfalls & Best Practices

Sponsored by GitAds