Testing async code and exceptions in xUnit: Assert.ThrowsAsync, Throws and ThrowsAny in C#

xUnit Testing & CI/CD in .NET November 13, 2026 csharp

A forgotten await on Assert.ThrowsAsync means the test always passes.

csharp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ======================================================================
// Q: How do you test async code and exceptions?
//
// - A forgotten await on Assert.ThrowsAsync means the test always passes. The
//   xUnit analyzer catches it (keep the analyzers enabled).
// - Never use .Result / .Wait() in tests: deadlocks and hidden
//   AggregateExceptions.
// ======================================================================

[Fact]
public async Task Place_without_stock_throws()          // async Task, never async void
{
    stock.TryReserveAsync("LIM-001", 5, Arg.Any<CancellationToken>()).Returns(false);

    var ex = await Assert.ThrowsAsync<DomainException>(
        () => handler.Handle(new PlaceOrder(orderId), CancellationToken.None));

    Assert.Contains("Insufficient stock", ex.Message);
}

// Synchronous: Assert.Throws<T> (exact type) or Assert.ThrowsAny<T> (subclasses too)
Assert.Throws<ArgumentException>(() => new Money(10m, "euro"));