Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Nov 16, 2025

Improvement to async iterator diagnostics - Complete ✅

This PR improves the diagnostic experience when an async iterator method (returning IAsyncEnumerable<T> or IAsyncEnumerator<T>) with yield statements is missing the async keyword.

Before:
Two redundant errors were reported:

error CS8403: Method 'C.M(int)' with an iterator block must be 'async' to return 'IAsyncEnumerator<int>'
error CS4032: The 'await' operator can only be used within an async method. 
Consider marking this method with the 'async' modifier and changing its return type to 'Task<IAsyncEnumerator<int>>'.

❌ Multiple errors with incorrect return type suggestion

After:
Single, clear error message:

error CS8403: Method 'C.M(int)' with an iterator block must be 'async' to return 'IAsyncEnumerator<int>'

✅ One focused error without incorrect return type suggestion

Changes

  • Modified Binder_Await.cs to skip reporting await errors for async iterators since ERR_IteratorMustBeAsync is already reported by ExecutableCodeBinder.ValidateIteratorMethod
  • Uses shared InMethodBinder.IsAsyncStreamInterface method to ensure consistent detection logic
  • This eliminates redundant error messages and improves the diagnostic experience
  • Added explanatory comment referencing where the primary error is reported
  • Updated tests to expect only one error (ERR_IteratorMustBeAsync) instead of two
  • Added test coverage for both IAsyncEnumerable<T> and IAsyncEnumerator<T> cases

Test Results

  • All 14 async iterator "returning" tests pass ✅
  • All 285 async iterator tests pass ✅

Fixes #31113

Original prompt

This section details on the original issue you should resolve

<issue_title>Async-streams: improve diagnostic when missing async keyword</issue_title>
<issue_description>```C#
[ConditionalFact(typeof(WindowsDesktopOnly))]
[WorkItem(31057, "https://github.com//issues/31057")]
public void AsyncIteratorReturningEnumerator_WithoutAsync()
{
string source = @"
class C
{
static System.Collections.Generic.IAsyncEnumerator M(int value)
{
yield return value;
await System.Threading.Tasks.Task.CompletedTask;
}
}";
var comp = CreateCompilationWithTasksExtensions(new[] { source, s_common });
comp.VerifyDiagnostics(
// (7,9): error CS4032: The 'await' operator can only be used within an async method. Consider marking this method with the 'async' modifier and changing its return type to 'Task<IAsyncEnumerator>'.
// await System.Threading.Tasks.Task.CompletedTask;
Diagnostic(ErrorCode.ERR_BadAwaitWithoutAsyncMethod, "await System.Threading.Tasks.Task.CompletedTask").WithArguments("System.Collections.Generic.IAsyncEnumerator").WithLocation(7, 9)
);
}


We can probably also improve this error (missing `IAsyncEnumerable<T>` or `IAsyncEnumerator<T>` type):
```C#
        [Fact]
        public void MissingTypeAndMembers_IAsyncEnumerator()
        {
...          
            VerifyMissingType(_enumerator, WellKnownType.System_Collections_Generic_IAsyncEnumerator_T,
                // (5,60): error CS1983: The return type of an async method must be void, Task, Task<T>, a task-like type, or IAsyncEnumerable<T>
                //     async System.Collections.Generic.IAsyncEnumerator<int> M() { await Task.CompletedTask; yield return 3; }
                Diagnostic(ErrorCode.ERR_BadAsyncReturn, "M").WithLocation(5, 60),
                // (5,60): error CS1624: The body of 'C.M()' cannot be an iterator block because 'IAsyncEnumerator<int>' is not an iterator interface type
                //     async System.Collections.Generic.IAsyncEnumerator<int> M() { await Task.CompletedTask; yield return 3; }
                Diagnostic(ErrorCode.ERR_BadIteratorReturn, "M").WithArguments("C.M()", "System.Collections.Generic.IAsyncEnumerator<int>").WithLocation(5, 60)
                );
...
        }
```</issue_description>

<agent_instructions>@copilot see if you can improve things here.  The issue is stating:

```c#
and changing its return type to 'Task<IAsyncEnumerator<int>>'.

Because tehre is a 'yield' statement in the method the correct return type is IAsyncEnumerator<> or IAsyncEnumerable<>. If the method already has one of tehse return types, we should just limit the error to say that 'async' should be added, and not the changing of the return type.</agent_instructions>

Comments on the Issue (you are @copilot in this section)


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Add new error code ERR_BadAwaitWithoutAsyncIterator (CS4037) that provides a better error message when an async iterator method is missing the 'async' keyword. The new error only suggests adding 'async' without suggesting to change the return type, since the return type is already correct for async iterators (IAsyncEnumerable<T> or IAsyncEnumerator<T>).

Co-authored-by: CyrusNajmabadi <[email protected]>
Copilot AI changed the title [WIP] Improve diagnostic message for missing async keyword Fix async iterator diagnostic to not suggest incorrect return type change Nov 16, 2025
Copilot AI requested a review from CyrusNajmabadi November 16, 2025 19:53
Copilot finished work on behalf of CyrusNajmabadi November 16, 2025 19:53
method.ReturnType.IsIAsyncEnumeratorType(Compilation)))
{
// For async iterators, don't suggest changing the return type
info = new CSDiagnosticInfo(ErrorCode.ERR_BadAwaitWithoutAsyncIterator);
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Nov 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot instead of a new error, can you use ERR_BadAwaitWithoutAsync? #Closed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 3de3769. Using ERR_BadAwaitWithoutAsync which provides a simpler message without suggesting a return type change.

[WorkItem(31113, "https://github.com/dotnet/roslyn/issues/31113")]
public void AsyncIteratorReturningEnumerable_WithoutAsync()
{
string source = @"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot use raw string literals.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in cac74c6. Converted both tests to use raw string literals and updated the line numbers in diagnostic expectations.

CyrusNajmabadi and others added 2 commits November 16, 2025 21:13
Address review feedback to use the existing ERR_BadAwaitWithoutAsync error instead of creating a new error code. This error already has an appropriate message for async iterators that doesn't suggest changing the return type.

Co-authored-by: CyrusNajmabadi <[email protected]>

// This error message is rather poor. Tracked by https://github.com/dotnet/roslyn/issues/31113
[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/31113")]
public void AsyncIteratorReturningEnumerable_WithoutAsync()
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Nov 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot add test with IAsyncEnumerator<...> as well. #Closed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both test cases are already present:

  • AsyncIteratorReturningEnumerator_WithoutAsync (line 1984) tests IAsyncEnumerator<int>
  • AsyncIteratorReturningEnumerable_WithoutAsync (line 2008) tests IAsyncEnumerable<int>

Both were added in commit 4ea5dc9.

Convert verbatim strings to raw string literals and update line numbers in diagnostic expectations accordingly.

Co-authored-by: CyrusNajmabadi <[email protected]>
{
info = new CSDiagnosticInfo(ErrorCode.ERR_BadAwaitWithoutVoidAsyncMethod);
}
else if (method.IsIterator &&
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Nov 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot fix formatting errors (excess spaces on line). #Closed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in b7d326e. Removed trailing whitespace from lines 192-194.

[WorkItem(31608, "https://github.com/dotnet/roslyn/issues/31608")]
public void AsyncIteratorReturningEnumerator_WithoutAsync()
{
string source = @"
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Nov 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot keep the original test string for tests you're updating. For new tests you can use raw-strings. #Closed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 3b78eef. Reverted the existing AsyncIteratorReturningEnumerator_WithoutAsync test to use verbatim strings. Only the new AsyncIteratorReturningEnumerable_WithoutAsync test uses raw string literals.

Remove trailing whitespace from the conditional expression in Binder_Await.cs.

Co-authored-by: CyrusNajmabadi <[email protected]>
Keep the original verbatim string format for the existing AsyncIteratorReturningEnumerator_WithoutAsync test. Only the new AsyncIteratorReturningEnumerable_WithoutAsync test uses raw string literals.

Co-authored-by: CyrusNajmabadi <[email protected]>
Copilot finished work on behalf of CyrusNajmabadi November 16, 2025 21:18
// (3,61): error CS8403: Method 'C.M(int)' with an iterator block must be 'async' to return 'IAsyncEnumerable<int>'
// static System.Collections.Generic.IAsyncEnumerable<int> M(int value)
Diagnostic(ErrorCode.ERR_IteratorMustBeAsync, "M").WithArguments("C.M(int)", "System.Collections.Generic.IAsyncEnumerable<int>").WithLocation(3, 61),
// (6,9): error CS1992: The 'await' operator can only be used when contained within a method or lambda expression marked with the 'async' modifier
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Nov 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot do we even need this error? It looks like we already report error CS8403: Method 'C.M(int)' with an iterator block must be 'async' to return 'IAsyncEnumerable<int>'. Since we already have that error, can we just not report this error on await? If so, update the code to check for that and not report, but give a comment in the code explaining why that's ok. #Closed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in a5766a3. Since ERR_IteratorMustBeAsync already tells the user to add 'async', we now skip reporting the await error to avoid redundancy. Added a comment explaining why this is okay.

For async iterators, ERR_IteratorMustBeAsync already tells the user to add 'async', so we don't need to also report an error on the await expression. This avoids redundant error messages.

Co-authored-by: CyrusNajmabadi <[email protected]>
Copilot AI changed the title Fix async iterator diagnostic to not suggest incorrect return type change Improve async iterator diagnostic by removing redundant error Nov 17, 2025
Copilot AI requested a review from CyrusNajmabadi November 17, 2025 00:07
Copilot finished work on behalf of CyrusNajmabadi November 17, 2025 00:07
else if (method.IsIterator &&
(method.ReturnType.IsIAsyncEnumerableType(Compilation) ||
method.ReturnType.IsIAsyncEnumeratorType(Compilation)))
{
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot mention which method reports this ERR_IteratorMustBeAsync error. Also, if possible, can we share code between that location and this location to ensure that we're checking all the same conditions to report an error there and not report an error here? #Closed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 5b4c38e. Now using the shared InMethodBinder.IsAsyncStreamInterface method to ensure we check the same conditions, and updated the comment to mention ExecutableCodeBinder.ValidateIteratorMethod as the location that reports the error.

Updated to use InMethodBinder.IsAsyncStreamInterface to share the same logic for detecting async stream interfaces, ensuring consistency between where the error is reported and where we skip reporting. Also updated the comment to mention ExecutableCodeBinder.ValidateIteratorMethod as the location that reports ERR_IteratorMustBeAsync.

Co-authored-by: CyrusNajmabadi <[email protected]>
Copilot finished work on behalf of CyrusNajmabadi November 17, 2025 00:15
Copilot AI requested a review from CyrusNajmabadi November 17, 2025 00:15
@CyrusNajmabadi CyrusNajmabadi marked this pull request as ready for review November 17, 2025 10:09
@CyrusNajmabadi CyrusNajmabadi requested a review from a team as a code owner November 17, 2025 10:09
@CyrusNajmabadi
Copy link
Member

@dotnet/roslyn-compiler ptal.

@jcouv jcouv self-assigned this Nov 17, 2025
@jcouv jcouv added the Feature - Async Streams Async Streams label Nov 17, 2025
Copy link
Member

@jcouv jcouv left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM Thanks (commit 11)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Async-streams: improve diagnostic when missing async keyword

3 participants