async
await
asynchronous programming
C#
awaitable methods

async/await. Where is continuation of awaitable part of method performed?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

When a C# method hits await, the rest of the method does not simply disappear and restart somewhere random later. The compiler splits the method into a state machine, and the continuation runs wherever the captured context or scheduler says it should run after the awaited operation completes.

What actually happens at await

Consider this method:

csharp
1using System;
2using System.Net.Http;
3using System.Threading.Tasks;
4
5public static async Task<string> FetchAsync(HttpClient client)
6{
7    Console.WriteLine("Before await");
8    string text = await client.GetStringAsync("https://example.com");
9    Console.WriteLine("After await");
10    return text;
11}

At the await, C# does not block the thread. Instead it:

  • checks whether the awaited task is already complete
  • if not, stores the current state of the method
  • registers the remainder of the method as a continuation
  • returns control to the caller

When the awaited task finishes, that continuation is scheduled to run.

Where the continuation runs

The short answer is: it depends on the current context.

In environments with a SynchronizationContext, such as UI frameworks, await usually captures that context. That means the continuation tries to resume on the original UI thread so code after the await can safely interact with UI state.

In environments without a special context, such as many console apps and ASP.NET Core request handlers, the continuation usually resumes on a thread pool thread.

So the continuation is not tied to "the same thread" as a guarantee. It is tied to the scheduling rules of the captured context, if one exists.

UI example versus server example

In a WinForms or WPF app:

  • 'await often captures the UI context'
  • the continuation resumes on the UI thread

In a console app:

  • there is usually no UI context to capture
  • the continuation resumes wherever the task scheduler chooses, commonly a thread pool thread

That difference explains why UI code can update controls after an await but library code should not assume a particular thread.

The role of ConfigureAwait(false)

If you do not want to resume on the captured context, use ConfigureAwait(false):

csharp
1using System.Net.Http;
2using System.Threading.Tasks;
3
4public static async Task<int> GetLengthAsync(HttpClient client)
5{
6    string text = await client
7        .GetStringAsync("https://example.com")
8        .ConfigureAwait(false);
9
10    return text.Length;
11}

This tells the awaiter not to marshal the continuation back to the original context. That is common in library code where there is no reason to return to a UI thread or request context.

It does not mean "run on a background thread." It means "do not require the original context for the continuation."

Why this matters for deadlocks and performance

Understanding continuation placement helps explain two common problems.

First, blocking on async code with .Result or .Wait() can deadlock in context-sensitive environments. The caller blocks the thread while the continuation is waiting to get back onto that same context.

Second, unnecessary context capture adds overhead. In library code that does not need it, ConfigureAwait(false) can remove that extra scheduling requirement.

A simple mental model

The safest mental model is:

  • 'await pauses the method logically, not the thread physically'
  • the rest of the method becomes a continuation
  • that continuation runs on the captured context if one is required
  • otherwise it runs on a scheduler-selected thread, often from the thread pool

That model is much more accurate than "await always resumes on the same thread."

Common Pitfalls

One common mistake is assuming await creates a new thread. It usually does not. It coordinates continuation scheduling around an existing asynchronous operation.

Another issue is assuming the continuation always runs on the same thread. That is often true in UI applications, but it is not a general language guarantee.

People also misuse ConfigureAwait(false) by treating it as a performance switch to sprinkle everywhere without understanding the code’s threading needs. In UI code, resuming off the UI context can break code that touches controls.

Finally, avoid mixing async code with blocking waits. Deadlocks caused by captured contexts are one of the oldest and most common async bugs in C#.

Summary

  • 'await turns the rest of the method into a continuation inside a compiler-generated state machine.'
  • The continuation runs according to the captured context or scheduler.
  • UI apps usually resume on the UI thread, while console and server apps often resume on thread pool threads.
  • 'ConfigureAwait(false) skips context capture when the continuation does not need it.'
  • 'await is about scheduling continuation, not about creating threads directly.'

Course illustration
Course illustration

All Rights Reserved.