Skip to content

Do not report MA0152 when the inner await suppresses the exceptions - #1484

Merged
meziantou merged 2 commits into
mainfrom
feature/ma0152-exception-suppression-612aa8
Sep 12, 2026
Merged

meziantou merged 2 commits into
mainfrom
feature/ma0152-exception-suppression-612aa8

Conversation

@meziantou

Copy link
Copy Markdown
Owner

Problem

MA0152 suggested replacing a double await with Unwrap(), and its code fix changed the behavior when the inner task was awaited with ConfigureAwaitOptions.SuppressThrowing:

Task<Task> outer = Task.FromException<Task>(new InvalidOperationException());
try
{
    await (await outer).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
    return "success";
}
catch (InvalidOperationException) { return "caught"; }

The outer await is outside the configured awaitable, so only the exceptions of the inner task are suppressed and the failure of outer still propagates: the snippet above returns caught. The code fix rewrote it to await outer.Unwrap().ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing), and since Unwrap() folds the faults of the outer task into the unwrapped one, SuppressThrowing swallows those too: the fixed code returns success. Both versions compile, so the exception was silently lost.

I confirmed the difference by executing both forms:

original: caught
fixed   : success

Fix

There is no Unwrap() form that preserves the semantics here, so this is fixed in the analyzer rather than in the fixer — reporting a diagnostic whose only code fix is unsound would be worse than not reporting it.

UseTaskUnwrapAnalyzer now bails out of the ConfigureAwait branch when the options may contain SuppressThrowing:

  • the diagnostic is still reported when the argument is a constant without the SuppressThrowing bit, so ConfigureAwait(false) and ConfigureAwait(ForceYielding | ContinueOnCapturedContext) are unaffected;
  • it is not reported when the options are not a constant (a variable or a parameter), as the flag cannot be ruled out.

The value of SuppressThrowing is read from the ConstantValue of the enum member instead of being hardcoded, and the whole check is inert on the target frameworks that do not have ConfigureAwaitOptions.

Notes for reviewers

  • The Task<Task<T>> case is covered by the same guard. Task<TResult>.ConfigureAwait rejects SuppressThrowing at run time anyway, so this only makes the two branches consistent.
  • docs/Rules/MA0152.md documents the case that is not reported. dotnet run --project src/DocumentationGenerator exits 0 afterwards.

Tests

Four tests added to UseTaskUnwrapAnalyzerTests: the plain SuppressThrowing case, a combined-flags case, a non-constant-options case, and a positive ForceYielding | ContinueOnCapturedContext case that still reports and fixes.

I checked the new tests are meaningful by neutralizing the guard: 3 of the 11 fail without it, all 11 pass with it.

11 tests passed, 0 failed, 0 skipped, on every Roslyn version (4.8, 4.14, 5.0, 5.6, 5.9). The full solution builds with 0 warnings and 0 errors. The whole test suite was not run locally; CI covers it.

`await (await outer).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing)`
only suppresses the exceptions of the inner task: the outer await is outside the
configured awaitable, so the failures of the outer task still propagate. The code
fix moved the options onto the unwrapped task, and `Unwrap()` folds the faults of
the outer task into it, so `SuppressThrowing` swallowed those too. Both versions
compile, so the transformation silently changed the behavior.

There is no `Unwrap()` form that preserves the semantics, so the fix is in the
analyzer: the diagnostic is no longer reported when the `ConfigureAwaitOptions`
argument is not a constant, or is a constant that has the `SuppressThrowing` bit
set. The value of the flag is read from the enum member, and the check is inert
on the target frameworks that do not have `ConfigureAwaitOptions`.
…ion-suppression-612aa8

# Conflicts:
#	tests/Meziantou.Analyzer.Test/Rules/UseTaskUnwrapAnalyzerTests.cs
@meziantou
meziantou merged commit eb8be5e into main Sep 12, 2026
13 checks passed
@meziantou
meziantou deleted the feature/ma0152-exception-suppression-612aa8 branch September 12, 2026 03:26
This was referenced Sep 12, 2026
This was referenced Sep 17, 2026
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.

1 participant