Skip to content

Add runtime async tests for catching non-Exception throws#130506

Draft
VSadov with Copilot wants to merge 4 commits into
mainfrom
copilot/runtime-async-tests-for-non-exceptions
Draft

Add runtime async tests for catching non-Exception throws#130506
VSadov with Copilot wants to merge 4 commits into
mainfrom
copilot/runtime-async-tests-for-non-exceptions

Conversation

Copilot AI commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Runtime async (async2) is meant to match compiler state-machine async (async1). No coverage existed for catching objects that don't derive from System.Exception (only throwable via IL), where RuntimeCompatibilityAttribute(WrapNonExceptionThrows) governs whether the object surfaces as a RuntimeWrappedException. These tests close that hole and pin down where the two forms agree and diverge.

Description

New tests under src/tests/async/runtime-wrapped-exception/:

  • NonExceptionThrower.il — IL helper that throws a non-Exception object (a string), since C# can't express this.
  • NoWrapThrowers.cs — helper assembly marked [assembly: RuntimeCompatibility(WrapNonExceptionThrows = false)] with async1/async2 throwers. The attribute is assembly-scoped, so an opt-out variant requires its own assembly.
  • runtime-wrapped-exception.cs — awaits each thrower (async1 via [RuntimeAsyncMethodGeneration(false)], async2 by default) and asserts the caller observes a RuntimeWrappedException wrapping the thrown object, for throws both before and after a suspension point, under both WrapNonExceptionThrows settings.

Observed behavior:

  • WrapNonExceptionThrows = true (C#/CoreLib default): async1 and async2 match — caller sees RuntimeWrappedException in all cases.
  • WrapNonExceptionThrows = false: they match for a synchronous (pre-yield) throw and for async2 after a yield. They diverge for a non-Exception thrown after a suspension point — async2 faults the Task with a RuntimeWrappedException, while async1 lets the raw object escape onto the thread pool and crash the process.

The divergent (crashing) case, CatchAfterYield_NoWrap_Async1, is disabled with [ActiveIssue("https://github.com/dotnet/runtime/issues/123194")], which unconditionally skips it and keeps it out of the merged runner so it can't abort the process. The remaining 7 tests run.

// async1 (state machine) vs async2 (runtime async) — same body, different generation
public static async Task ThrowAfterYieldAsync2()
{
    await Task.Yield();
    NonExceptionThrower.ThrowNonException(); // throws a string (IL)
}

[RuntimeAsyncMethodGeneration(false)]
public static async Task ThrowAfterYieldAsync1()
{
    await Task.Yield();
    NonExceptionThrower.ThrowNonException();
}

Copilot AI requested review from Copilot and removed request for Copilot July 10, 2026 17:27
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @agocke
See info in area-owners.md if you want to be subscribed.

Co-authored-by: VSadov <8218165+VSadov@users.noreply.github.com>
Copilot AI requested review from Copilot and removed request for Copilot July 10, 2026 18:03
Co-authored-by: VSadov <8218165+VSadov@users.noreply.github.com>
Copilot AI requested review from Copilot and removed request for Copilot July 10, 2026 18:14
Copilot AI changed the title [WIP] Add tests for runtime async non-exception behaviors Add runtime async tests for catching non-Exception throws Jul 10, 2026
Copilot AI requested a review from VSadov July 10, 2026 18:15
Copilot AI temporarily deployed to copilot-pat-pool July 10, 2026 21:47 Inactive
Copilot AI temporarily deployed to copilot-pat-pool July 10, 2026 21:47 Inactive
Comment on lines +56 to +58
// * async1 lets the raw non-Exception escape the resumed state machine onto
// the thread pool where it becomes an unhandled exception and crashes the
// process, so it can never be observed by the caller.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

async1 lets the raw non-Exception escape the resumed state machine onto
the thread pool where it becomes an unhandled exception and crashes the
process

async2 behavior makes more sense.
This is a corner case though. Not a lot of value in changing async1 behavior here.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot let's

  • keep the async2 behavior expectations as-is,
  • keep the comment on differences,
  • keep the async 1 scenario blocked, but not on an issue since we are not going to fix, block on some short comment like "async1 behavior is terminal to the process, thus we will not test it".

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 8fef2c4. Kept the async2 expectations and the difference comment as-is, and the async1 divergent case stays blocked — now via the comment "async1 behavior here is terminal to the process, thus we will not test it" instead of the [ActiveIssue] link.

Note

This comment was generated by GitHub Copilot.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Correction: the change is in commit 362e9c2 (not 8fef2c4).

Note

This comment was generated by GitHub Copilot.

Co-authored-by: VSadov <8218165+VSadov@users.noreply.github.com>
Copilot AI requested review from Copilot and removed request for Copilot July 11, 2026 01:41
Copilot AI requested a review from VSadov July 11, 2026 01:42
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.

Runtime async needs tests for catching non-exceptions which are thrown

2 participants