Skip to content

Document Throws/ContainsSingle null return inside Assert.Scope in RFC 011#9186

Merged
Evangelink merged 1 commit into
mainfrom
dev/amauryleve/rfc011-throws-soft-assertions
Jun 16, 2026
Merged

Document Throws/ContainsSingle null return inside Assert.Scope in RFC 011#9186
Evangelink merged 1 commit into
mainfrom
dev/amauryleve/rfc011-throws-soft-assertions

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Follow-up to discussion on #9069.

RFC 011 documented that value-returning assertions return null/default when they fail inside an Assert.Scope(), but only named ContainsSingle. The Assert.Throws/ThrowsExactly/ThrowsAsync/ThrowsExactlyAsync family has the identical behavior (return null!) yet wasn't mentioned anywhere in the RFC, which is what made the behavior in #9069 look like a bug.

This PR:

  • Adds the Throws* family alongside ContainsSingle in the value-returning assertion notes (Option 2 list and the chosen Option 3 all assertions participate in soft collection list).
  • Adds a new Value-returning assertions return null/default in scoped mode subsection with a Throws example showing the NullReferenceException-as-secondary-symptom behavior.

No behavior change — documentation only. The corresponding user-facing docs are in dotnet/docs#54409.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 16, 2026 14:16
@Evangelink Evangelink enabled auto-merge (squash) June 16, 2026 14:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Updates RFC 011 documentation to explicitly call out that the Assert.Throws* family behaves like other value-returning assertions (e.g., ContainsSingle) under Assert.Scope(): failures are collected and the API returns null/default rather than throwing immediately, which can lead to secondary downstream symptoms if the return value is used before scope disposal.

Changes:

  • Adds Throws<T> / ThrowsExactly<T> / ThrowsAsync<T> / ThrowsExactlyAsync<T> to the RFC’s value-returning assertion discussion (Option 2 and Option 3 sections).
  • Adds a new “Value-returning assertions return null/default in scoped mode” subsection with an illustrative Throws example and guidance.
Show a summary per file
File Description
docs/RFCs/011-Soft-Assertions-Nullability-Design.md Expands RFC 011 to document Throws* null/default return behavior inside Assert.Scope() and provides an example + guidance.

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 0

@Evangelink Evangelink merged commit 45e7963 into main Jun 16, 2026
13 checks passed
@Evangelink Evangelink deleted the dev/amauryleve/rfc011-throws-soft-assertions branch June 16, 2026 14:20

@Evangelink Evangelink left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Note

🤖 Automated review by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.

# Dimension Verdict
17 Documentation Accuracy 🔵 1 NIT

✅ 21/22 dimensions clean.

  • Documentation Accuracy — "(except Fail/Inconclusive)" parenthetical omits null precondition checks as a second hard-exception class (inline comment on line 193)

Review notes (documentation-only PR)

Dimensions 1–16, 18–22 are N/A — this PR changes only docs/RFCs/011-Soft-Assertions-Nullability-Design.md with no code, tests, build scripts, or serialization formats.

Dimension 17 — Documentation Accuracy: The three changes are accurate and consistent with the implementation.

  1. Tier 1 bullet (Option 2, rejected) — line 87: Correctly adds the Throws* family alongside ContainsSingle to the "returning null causes downstream errors" rationale in the rejected option's Tier 1 list. The phrasing "returning null" (rather than "null/default(T)") is intentionally tighter than the ContainsSingle entry — T is constrained to Exception (a reference type) so default(T) is always null. Accurate.

  2. Pros bullet (Option 3, chosen) — line 109: Correctly adds all four async/sync variants to the enumerated list of soft assertions. Preserves the existing sort order (ContainsSingle → Throws family → IsTrue/IsFalse). Accurate.

  3. New subsection "Value-returning assertions return null/default in scoped mode" — lines 186–209: The behaviour description and code example are accurate and confirmed by the implementation in Assert.ThrowsException.Core.cs, which does return null! after ReportAssertFailed returns (soft mode). The using form is appropriate for the synchronous Throws<T> example; the async variants (ThrowsAsync<T>, ThrowsExactlyAsync<T>) are named in the text. The guidance "move the call outside the scope, or restructure so nothing depends on the return value until after the scope is disposed" is both correct and actionable — if the assertion succeeds (exception thrown), the scope collects no failure and Dispose does not throw, so post-scope code can safely use the return value; if it fails, Dispose throws before post-scope code runs, providing the intended protection.

One NIT filed inline (see above): the "(except Fail/Inconclusive)" shorthand omits the also-documented CheckParameterNotNull precondition checks. This is a completeness suggestion, not a correctness problem — the full exception list is already given in the preceding bullet points.

🤖 Automated content by GitHub Copilot. Posted via a maintainer's GitHub token, so it appears under their account — the account owner did not write or approve this content personally. Generated by the Expert Code Review (on PR ready) workflow. · 489.3 AIC · ⌖ 12.4 AIC ·

- `Assert.ContainsSingle(...)` returns the single matched element.
- `Assert.Throws<T>(...)`, `Assert.ThrowsExactly<T>(...)`, `Assert.ThrowsAsync<T>(...)`, and `Assert.ThrowsExactlyAsync<T>(...)` return the caught exception.

These are soft within a scope, like every other assertion (except `Fail`/`Inconclusive`). When such an assertion *fails* inside a scope, the failure is collected and execution continues — so the method returns `null`/`default(T)` rather than throwing. The non-null return contract (and any nullable annotation) is intentionally not enforced in scoped mode, exactly as described above for all other postconditions.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

[NIT] Documentation Accuracy

The parenthetical "(except Fail/Inconclusive)" omits the second class of hard exceptions already documented in the bullet points immediately above: null precondition checks (CheckParameterNotNull) inside Assert APIs also throw directly rather than collecting. A reader encountering this sentence in isolation could reasonably conclude that only Fail/Inconclusive are exceptions to the soft-collection rule.

Recommendation: Expand to keep the statement self-contained:

These are soft within a scope, like every other assertion (except `Fail`/`Inconclusive` and null precondition checks).

Alternatively, add a parenthetical cross-reference: "(see exceptions listed above)" to signal explicitly that the full list is already documented.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants