.NETHard
How does async/await actually work in C#? Compiler-generated state machines explained
The compiler rewrites every async method into a state machine that implements IAsyncStateMachine. Each await becomes a possible suspension point.
What the compiler generates
public async Task<int> GetAsync()
{
var data = await _http.GetStringAsync("https://api");
return data.Length;
}
Becomes (simplified):
private struct StateMachine : IAsyncStateMachine
{
public int state;
public AsyncTaskMethodBuilder<int> builder;
public TaskAwaiter<string> awaiter;
public string data;
public void MoveNext() {
if (state == -1) {
awaiter = _http.GetStringAsync("https://api").GetAwaiter();
if (!awaiter.IsCompleted) {
state = 0;
builder.AwaitUnsafeOnCompleted(ref awaiter, ref this);
return;
}
}
data = awaiter.GetResult();
builder.SetResult(data.Length);
}
}
Key insights
- No new thread. The continuation runs on whatever thread completes the awaited operation. With
ConfigureAwait(false), on the thread pool; without it, on the capturedSynchronizationContext(UI thread in WPF/WinForms, request context in legacy ASP.NET). awaitis NOTWait().Wait()blocks.awaitreturns to the caller and resumes when the task completes.- Allocation cost. Each async method allocates a state-machine struct, boxed when it actually suspends. Hot paths use
ValueTask<T>to skip the allocation when the operation completes synchronously. - Exceptions are captured into the returned
Task— they only surface when youawaitit. Fire-and-forget loses them silently.
Common interview follow-ups
| Question | Answer |
|---|---|
Why does async void exist? | Event handlers only. Exceptions crash the process. |
What is SynchronizationContext? | Where continuations run by default. ConfigureAwait(false) skips it. |
Task.Run inside async? | Almost always wrong. Adds a thread hop with zero gain. |
Why ValueTask? | Stack-allocated; avoid heap allocation for hot synchronous paths. |