DEV Community

Ivan Rossouw
Ivan Rossouw

Posted on

Navigation Is a Commit Marker: Handling Partial Success in Blazor

Navigation looks harmless because it sits at the edge of the code: call a method, change the route, and continue the journey.

But a redirect is not merely presentation. It tells the user something important about the work they just asked the system to do.

If the application moves to the next screen, most people reasonably infer that the previous step completed. That makes navigation a small but meaningful commit marker.

The hidden contract in a redirect

Consider a Blazor component that accepts several entries and adds each one to a shared collection before opening a review screen. The operations are independent. One request might contain three entries, producing three results.

The tempting implementation is to remember whether anything succeeded:

var addedAny = false;

foreach (var entry in entries)
{
    var result = await AddAsync(entry);
    addedAny |= result.Succeeded;
}

if (addedAny)
{
    Navigation.NavigateTo("/review");
}
Enter fullscreen mode Exit fullscreen mode

The code is tidy, but its postcondition is wrong. “At least one operation succeeded” is not the same as “the requested step completed.”

Imagine that the first two additions succeed and the third returns a validation failure. The destination screen now shows a plausible-looking collection, while the failed entry has disappeared from view. The interface has converted a partial outcome into an apparent success.

Nothing necessarily crashed. No exception is required. The bug is a mismatch between the route transition and the truth of the workflow.

Make the completion rule explicit

The stronger design begins by naming the state that permits navigation:

var outcomes = new List<OperationOutcome>();

foreach (var entry in entries)
{
    outcomes.Add(await AddAsync(entry));
}

if (outcomes.All(x => x.Succeeded))
{
    Navigation.NavigateTo("/review");
    return;
}

ShowFailures(outcomes);
Enter fullscreen mode Exit fullscreen mode

This does not make the batch atomic. It does something more modest and still valuable: it stops claiming full completion when the evidence says otherwise.

For a mixed outcome, the component can remain on the current screen, show the failed entries, retain confirmation that some work succeeded, and offer a deliberate route to the next step. The user keeps context and can decide whether to correct, retry, or continue with the accepted subset.

Exceptions should enter the same outcome model. If result objects are collected but transport exceptions vanish into an empty catch, “no recorded failure” can accidentally become success. Normalize every expected failure path before evaluating the navigation rule.

State deserves a name

A pair of booleans often starts innocently and then becomes difficult to reason about. For example, isBusy, addedAny, and a list of errors can represent more combinations than the UI actually supports.

An explicit state model makes the contract easier to see:

enum SubmissionState
{
    Idle,
    Working,
    Complete,
    Failed,
    PartiallyComplete
}
Enter fullscreen mode Exit fullscreen mode

The exact type is less important than the discipline. Each state should have a clear visual treatment and a clear set of allowed actions.

  • Complete may navigate automatically.
  • Failed stays put and explains what needs attention.
  • PartiallyComplete preserves both truths: some work succeeded, and the requested batch did not fully complete.

That last state is the one optimistic interfaces tend to erase.

Test the transition, not only the calls

Component tests often prove that a dependency was invoked with the right data. That is useful, but it does not prove the user journey.

For a batch followed by navigation, pin at least these behaviours:

  1. Full success performs every required operation and reaches the intended destination.
  2. Total failure stays on the current route and displays a useful error.
  3. Mixed success stays on the current route, preserves visible evidence of accepted work, and presents an intentional next action.
  4. An empty or invalid submission performs no operation and does not navigate.
  5. A second click while work is running cannot start a competing batch.

In bUnit, asserting the fake navigation manager’s final URI is as important as verifying the service calls. Also assert the recovery UI. “Did not navigate” without a usable next step can still leave the user stranded.

The trade-off is real

This design introduces more component state, more copy, and more tests. A partial outcome also forces a product decision: should the user retry only the failures, abandon the accepted subset, or continue deliberately?

An atomic server-side command can simplify the interface when all operations belong to one consistency boundary. But that option may be unavailable when calls cross services, external systems, or independently committed resources. In those cases, pretending the batch is atomic in the UI does not create atomicity. It only hides uncertainty.

The practical compromise is to make the completion predicate explicit, preserve accurate state, and define retry behaviour before shipping the happy-path redirect.

A small review question with wide reach

When reviewing a navigation call, ask:

What fact has just become true that makes this destination honest?

If the answer is “the handler finished” or “something succeeded,” the transition probably deserves another look.

Treating navigation as a commit marker does not require a new framework. It requires aligning a route change with a tested postcondition. That small shift makes partial failures visible, recovery deliberate, and the interface more trustworthy.

Top comments (0)