C#

C 5 async CTP why is internal state set to 0 in generated code before EndAwait call

27 September 2026 · 10 min read

C 5 async CTP why is internal state set to 0 in generated code before EndAwait call

Delving into the intricate world of C async and await reveals a sophisticated compiler-generated state machine working silently behind the scenes. Developers often leverage these keywords for asynchronous operations without needing to understand their deep internal mechanics. However, for those seeking to truly master C or debug complex asynchronous issues, understanding the compiler’s output is invaluable. A particularly intriguing aspect, especially when examining the early C 5 async CTP implementations, is observing why the internal "state" variable is frequently set to 0 in the generated code just before an EndAwait call. This behavior isn’t arbitrary; it reflects a carefully designed mechanism for managing execution flow, ensuring proper cleanup, and handling potential re-entrancy or error conditions within the asynchronous context. Understanding this subtle detail sheds light on the robustness of C’s asynchronous programming model.

Unpacking the Async/Await State Machine

When you mark a method with the async keyword and use await within it, the C compiler doesn’t simply execute your code line by line. Instead, it performs a significant transformation, converting your method into a complex state machine. This state machine is essentially a class generated by the compiler that implements the IAsyncStateMachine interface. Its primary role is to manage the execution flow of your asynchronous method across multiple await points, effectively pausing execution when an awaited operation is pending and resuming it once the operation completes.

Each time an await expression is encountered, the state machine captures the current context and “yields” control back to the caller. When the awaited operation finishes, the state machine is reactivated, and execution continues from the exact point it left off. This intricate orchestration relies heavily on an internal state variable, which serves as a pointer to the current position within the asynchronous method’s logical flow. Without this state management, resuming execution after an asynchronous delay would be incredibly challenging, if not impossible, within the confines of a single method call.

The generated state machine class contains fields to store local variables and parameters that need to persist across await boundaries, ensuring data integrity as execution hops between threads or different points in time. This transformation is a cornerstone of the Task-based Asynchronous Pattern (TAP) in C, making asynchronous programming significantly more approachable than earlier models like the Asynchronous Programming Model (APM) or Event-based Asynchronous Pattern (EAP).

The Role of the Internal ‘State’ Variable

Within the compiler-generated state machine, the internal state variable is paramount. It’s typically an integer field, often initialized to -1, representing that the method has not yet started or has completed. As the asynchronous method progresses, this state variable is updated to reflect the specific “state” or execution point of the method. For instance, a state of 0 might indicate the initial execution block, while 1, 2, etc., would correspond to the code segments immediately following different await expressions.

When an await operation begins, the state machine saves the current state value, prepares to pause, and schedules a continuation. Upon completion of the awaited task, the continuation mechanism invokes the state machine’s MoveNext() method. Inside MoveNext(), a large switch statement or similar branching logic inspects the value of the state variable to determine where execution needs to resume. This is the fundamental mechanism that allows an async method to “jump” back to the correct line of code after an asynchronous operation has finished.

The careful manipulation of this state variable ensures that the logical flow of your asynchronous method is preserved, even though the actual execution might be spread across different threads and timeframes. It acts as a bookmark, allowing the compiler to navigate the generated code effectively and maintain the illusion of linear execution within your asynchronous method. Understanding this internal state management is key to debugging and optimizing complex asynchronous workflows.

Why State is Set to 0 Before EndAwait

The practice of setting the internal state variable to 0 just before an EndAwait call in the generated C 5 async CTP code is a crucial defensive and cleanup mechanism. This reset ensures that the state machine is returned to a known, neutral position (often representing the method’s initial or completion state) after an awaited operation has concluded its core work, preventing unintended re-entry or signaling that the awaitable has been fully processed. Specifically, if an awaited task completes successfully, the state machine transitions to the code immediately following the await. Before the actual results are extracted and potential exceptions handled by EndAwait, resetting the state preemptively signals that the specific await point has been fully handled.

This behavior is particularly relevant in scenarios where an awaitable might be subject to multiple continuations or where its internal state needs to be tidied up. By setting state to 0 (or -1, depending on the exact compiler version and context), the state machine indicates that the specific segment of code associated with the completed await is finished. This makes the state machine ready for its next logical step, whether that’s proceeding to the next await, returning a result, or propagating an exception. It’s a subtle but important detail in the compiler’s design for robust asynchronous control flow, helping to prevent logical errors or unexpected behavior if the state machine were to somehow be re-invoked for that same await point.

This design choice in the early CTPs, and its subsequent evolution, highlights the compiler’s role in abstracting away the complexities of asynchronous execution. It ensures that even in the face of rapid task completions or intricate continuation chains, the state machine consistently knows its place. For a deeper dive into these internal mechanics, resources like the [the GetAwaiter() / BeginAwait() / EndAwait() calls.](<https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/task-asynchronous-programming Question & Answer :

Yesterday I was giving a talk about the new C# >)

We looked in some detail at the state machine generated by the C# compiler, and there were two aspects we couldn’t understand:

  • Why the generated class contains a Dispose() method and a $__disposing variable, which never appear to be used (and the class doesn’t implement IDisposable).
  • Why the internal state variable is set to 0 before any call to EndAwait(), when 0 normally appears to mean “this is the initial entry point”.

I suspect the first point could be answered by doing something more interesting within the async method, although if anyone has any further information I’d be glad to hear it. This question is more about the second point, however.

Here’s a very simple piece of sample code:

using System.Threading.Tasks; class Test { static async Task<int> Sum(Task<int> t1, Task<int> t2) { return await t1 + await t2; } } 

… and here’s the code which gets generated for the MoveNext() method which implements the state machine. This is copied directly from Reflector - I haven’t fixed up the unspeakable variable names:

public void MoveNext() { try { this.$__doFinallyBodies = true; switch (this.<>1__state) { case 1: break; case 2: goto Label_00DA; case -1: return; default: this.<a1>t__$await2 = this.t1.GetAwaiter<int>(); this.<>1__state = 1; this.$__doFinallyBodies = false; if (this.<a1>t__$await2.BeginAwait(this.MoveNextDelegate)) { return; } this.$__doFinallyBodies = true; break; } this.<>1__state = 0; this.<1>t__$await1 = this.<a1>t__$await2.EndAwait(); this.<a2>t__$await4 = this.t2.GetAwaiter<int>(); this.<>1__state = 2; this.$__doFinallyBodies = false; if (this.<a2>t__$await4.BeginAwait(this.MoveNextDelegate)) { return; } this.$__doFinallyBodies = true; Label_00DA: this.<>1__state = 0; this.<2>t__$await3 = this.<a2>t__$await4.EndAwait(); this.<>1__state = -1; this.$builder.SetResult(this.<1>t__$await1 + this.<2>t__$await3); } catch (Exception exception) { this.<>1__state = -1; this.$builder.SetException(exception); } } 

It’s long, but the important lines for this question are these:

// End of awaiting t1 this.<>1__state = 0; this.<1>t__$await1 = this.<a1>t__$await2.EndAwait(); // End of awaiting t2 this.<>1__state = 0; this.<2>t__$await3 = this.<a2>t__$await4.EndAwait(); 

In both cases the state is changed again afterwards before it’s next obviously observed… so why set it to 0 at all? If MoveNext() were called again at this point (either directly or via Dispose) it would effectively start the async method again, which would be wholly inappropriate as far as I can tell… if and MoveNext() isn’t called, the change in state is irrelevant.

Is this simply a side-effect of the compiler reusing iterator block generation code for async, where it may have a more obvious explanation?

Important disclaimer

Obviously this is just a CTP compiler. I fully expect things to change before the final release - and possibly even before the next CTP release. This question is in no way trying to claim this is a flaw in the C# compiler or anything like that. I’m just trying to work out whether there’s a subtle reason for this that I’ve missed :)

Okay, I finally have a real answer. I sort of worked it out on my own, but only after Lucian Wischik from the VB part of the team confirmed that there really is a good reason for it. Many thanks to him - and please visit his blog (on archive.org), which rocks.

The value 0 here is only special because it’s not a valid state which you might be in just before the await in a normal case. In particular, it’s not a state which the state machine may end up testing for elsewhere. I believe that using any non-positive value would work just as well: -1 isn’t used for this as it’s logically incorrect, as -1 normally means “finished”. I could argue that we’re giving an extra meaning to state 0 at the moment, but ultimately it doesn’t really matter. The point of this question was finding out why the state is being set at all.

The value is relevant if the await ends in an exception which is caught. We can end up coming back to the same await statement again, but we mustn’t be in the state meaning “I’m just about to come back from that await” as otherwise all kinds of code would be skipped. It’s simplest to show this with an example. Note that I’m now using the second CTP, so the generated code is slightly different to that in the question.

Here’s the async method:

static async Task<int> FooAsync() { var t = new SimpleAwaitable(); for (int i = 0; i < 3; i++) { try { Console.WriteLine("In Try"); return await t; } catch (Exception) { Console.WriteLine("Trying again..."); } } return 0; } 

Conceptually, the SimpleAwaitable can be any awaitable - maybe a task, maybe something else. For the purposes of my tests, it always returns false for IsCompleted, and throws an exception in GetResult.

Here’s the generated code for MoveNext:

public void MoveNext() { int returnValue; try { int num3 = state; if (num3 == 1) { goto Label_ContinuationPoint; } if (state == -1) { return; } t = new SimpleAwaitable(); i = 0; Label_ContinuationPoint: while (i < 3) { // Label_ContinuationPoint: should be here try { num3 = state; if (num3 != 1) { Console.WriteLine("In Try"); awaiter = t.GetAwaiter(); if (!awaiter.IsCompleted) { state = 1; awaiter.OnCompleted(MoveNextDelegate); return; } } else { state = 0; } int result = awaiter.GetResult(); awaiter = null; returnValue = result; goto Label_ReturnStatement; } catch (Exception) { Console.WriteLine("Trying again..."); } i++; } returnValue = 0; } catch (Exception exception) { state = -1; Builder.SetException(exception); return; } Label_ReturnStatement: state = -1; Builder.SetResult(returnValue); } 

I had to move Label_ContinuationPoint to make it valid code - otherwise it’s not in the scope of the goto statement - but that doesn’t affect the answer.

Think about what happens when GetResult throws its exception. We’ll go through the catch block, increment i, and then loop round again (assuming i is still less than 3). We’re still in whatever state we were before the GetResult call… but when we get inside the try block we must print “In Try” and call GetAwaiter again… and we’ll only do that if state isn’t 1. Without the state = 0 assignment, it will use the existing awaiter and skip the Console.WriteLine call.

It’s a fairly tortuous bit of code to work through, but that just goes to show the kinds of thing that the team has to think about. I’m glad I’m not responsible for implementing this :)