---
title: 'CHR0039: Assertion result is discarded and can never fail'
---

import { Tabs, TabItem } from '@astrojs/starlight/components';

## Rule Description

The event-sequence assertions in `Cratis.Chronicle.Testing` — `ShouldHaveAppendedEvent<T>(...)` and `ShouldHaveTailSequenceNumber(...)`, in every overload — return a `Task`. They signal failure by throwing an `EventSequenceAssertionException` on that `Task`. The kernel-backed integration assertions in `Cratis.Chronicle.XUnit.Integration.Events` — `ShouldHaveAppendedEvent<T>(...)`, `ShouldHaveNextSequenceNumber(...)` and `ShouldHaveTailSequenceNumber(...)` — work the same way.

Call one from a `void`-bodied fact and the awaitable is discarded, so the exception is never observed. The assertion becomes a no-op that passes no matter what the system under test does.

Await the assertion and declare the containing member `async Task`, or return the `Task` so the test runner awaits it.

## Severity

Warning

## Example

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
using Cratis.Chronicle.Events;
using Cratis.Chronicle.EventSequences;
using Cratis.Chronicle.Testing.EventSequences;
using Xunit;

[EventType]
public record Chr0039AuthorRegistered(string Name);

public class Chr0039WhenRegisteringAnAuthor
{
    readonly IEventLog _eventLog = default!;

    // Warning CHR0039: 'ShouldHaveAppendedEvent' returns an awaitable that is never awaited, so the
    // assertion can never fail. The fact is 'void', so the compiler's own CS4014 stays silent —
    // this spec passes even though no Chr0039AuthorRegistered carries that name.
    [Fact]
    void should_have_appended_the_event() =>
        _eventLog.ShouldHaveAppendedEvent<Chr0039AuthorRegistered>(e => e.Name == "Jane Austen");

    // Warning CHR0039: the same trap in a block body.
    [Fact]
    void should_have_appended_exactly_one_event()
    {
        _eventLog.ShouldHaveTailSequenceNumber(EventSequenceNumber.First);
    }
}
```

</TabItem>
</Tabs>

The fix:

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
using Cratis.Chronicle.Events;
using Cratis.Chronicle.EventSequences;
using Cratis.Chronicle.Testing.EventSequences;
using Xunit;

[EventType]
public record Chr0039AuthorRegisteredFixed(string Name);

public class Chr0039WhenRegisteringAnAuthorFixed
{
    readonly IEventLog _eventLog = default!;

    // Declaring the fact 'async Task' and awaiting the assertion makes the exception it throws
    // observable, so the assertion can actually fail.
    [Fact]
    async Task should_have_appended_the_event() =>
        await _eventLog.ShouldHaveAppendedEvent<Chr0039AuthorRegisteredFixed>(e => e.Name == "Jane Austen");

    // Returning the Task works too — the test runner awaits it.
    [Fact]
    Task should_have_appended_exactly_one_event() =>
        _eventLog.ShouldHaveTailSequenceNumber(EventSequenceNumber.First);
}
```

</TabItem>
</Tabs>

## Why This Rule Exists

The compiler already has a rule for a discarded `Task` — CS4014 — but it fires **only inside an `async` method**. A `void`-bodied fact is the natural and dominant shape of a spec, and it is exactly the shape where CS4014 says nothing. A zero-warning build is therefore blind to it.

What makes this a genuine trap rather than ordinary async carelessness is that the *sibling* assertions on the same surface are synchronous and `void`:

- `ShouldBeSuccessful()`
- `ShouldHaveValidationErrors()`
- `ShouldHaveConstraintViolation()` / `ShouldNotHaveConstraintViolation()`
- `ShouldHaveConcurrencyViolations()`

Discarding "the result" of those is exactly how they are meant to be used. Nothing at the call site signals that the event-sequence assertions behave differently, so the two shapes sit side by side in the same file and only one of them works.

The failure mode is the worst one a test suite has: an assertion that appears to test something and tests nothing. The resulting false confidence survives review, CI, and time — and it hides real defects, because the assertion that would have caught a regression silently passes instead.

The rule is scoped to `Should*` methods on the Cratis testing surfaces — namespaces under `Cratis.` containing a `.Testing.` or `.XUnit.` segment — that return `Task`, `Task<T>`, `ValueTask` or `ValueTask<T>`, and only where the result is genuinely discarded: a statement on its own line, or the expression body of a `void` member. Awaiting, returning, assigning, or passing the awaitable along all count as observing it and are not flagged, and a `Should*` API outside those namespaces is never flagged.

## Related Rules

- [Writing specs](/chronicle/testing/) — the testing surfaces this rule covers.
