Teach Cursor Result<T> instead of throwing

If your team already models failures as Result<T>, ErrorOr<T>, or railway-style responses, Cursor will still reach for throw and null on the next prompt. That is not malice — it is training bias. Here is why it happens, what it costs, and how scoped rules teach the AI to match the error model you already paid for.

The default the model learned

Most C# examples on the public internet — tutorials, StackOverflow, even Microsoft docs samples — use exceptions for business failures and null for "not found". Ask Cursor to add an endpoint that rejects duplicate orders and you will get something like:

public async Task<OrderDto> CreateAsync(CreateOrderRequest request, CancellationToken ct)
{
    var existing = await _db.Orders.FirstOrDefaultAsync(o => o.Reference == request.Reference, ct);
    if (existing is not null)
        throw new ConflictException("Order reference already exists");

    var order = Order.Create(request);
    await _db.SaveChangesAsync(ct);
    return order.ToDto();
}

Readable. Familiar. Architecturally wrong if your Application layer already returns Result<OrderDto> and your API maps errors to ProblemDetails without catching domain exceptions in every controller.

What breaks when the AI throws anyway

  • Inconsistent HTTP semantics. Some endpoints return 409 from a mapper; others leak 500s because an exception bubbled past the pipeline you thought was uniform.
  • Untestable handlers. Unit tests for MediatR handlers should assert on result.IsError, not Assert.ThrowsAsync for business cases.
  • Hidden control flow. null checks and thrown exceptions are invisible in signatures. The AI (and the next human) cannot see failure modes without reading the body.
  • Retry poison. Transient infrastructure failures belong in exceptions; business rule violations do not. Mixing them trains operators to retry non-retryable faults.

Senior teams moved to explicit results precisely to make failure visible. The AI undoing that in one autocomplete is expensive.

What good looks like in a MediatR codebase

Same feature, result-shaped:

public async Task<Result<OrderDto>> Handle(CreateOrderCommand cmd, CancellationToken ct)
{
    var exists = await _orders.ExistsByReferenceAsync(cmd.Reference, ct);
    if (exists)
        return Result.Conflict<OrderDto>("Order reference already exists");

    var create = Order.Create(cmd);
    if (create.IsError)
        return create.Errors;

    await _orders.AddAsync(create.Value, ct);
    return create.Value.ToDto();
}

The endpoint stays thin:

app.MapPost("/orders", async (CreateOrderCommand cmd, ISender sender, CancellationToken ct) =>
{
    var result = await sender.Send(cmd, ct);
    return result.Match(Results.Created, Results.Problem);
});

No try/catch for "customer not found". No null return that the caller forgets to check. The signature documents the contract.

Why telling it once does not stick

You paste "we use Result pattern, do not throw for business errors" into chat. It complies for that file. Three prompts later, on a validator or a repository method, it throws NotFoundException again because:

  1. The local context is a file that still has legacy throws from 2019.
  2. The prompt did not mention ErrorOr vs Result vs FluentResults — so it picks whichever type name it saw most recently in training.
  3. There is no enforced rule on Application-layer files — only a memory in a chat you closed yesterday.

This is the same persistence problem as the Context Tax, applied to error modelling. You need the convention to reload when the relevant layer opens — not when you remember to lecture the model.

The rule contract (what to encode)

A useful Cursor rule for result-shaped codebases does not need to mandate a specific NuGet package. It should:

  • Detect the house style — if the project already references FluentResults, ErrorOr, or an internal Result<T>, match that type exactly.
  • Forbid null-as-missing on public Application APIs when Results are in use — return NotFound errors instead of null.
  • Reserve exceptions for truly exceptional cases: programmer errors, cancelled operations, infrastructure timeouts — not "email already taken".
  • Require mapping at the edge — handlers return Results; endpoints map them. No throws in Minimal API lambdas for domain failures.
  • Keep validators ahead of handlers — FluentValidation failures should become Results before handler logic runs (pairs well with MediatR pipelines).

arch-core.mdc in the Agentic Architect kit encodes the "match existing Result / ErrorOr / OneOf patterns" clause on Application and API-adjacent files. It is the boundary guardian applied to control flow — not just folder placement.

A prompt you can use today (before the full kit)

Until rules are committed, pin this at the top of any handler or endpoint edit:

Business failures are Result errors, not exceptions. Match the Result/ErrorOr type already used in this project. Map to HTTP at the API boundary only.

Short. Boring. Repeatable. It cuts throw regressions roughly in half in my experience — but discipline still decays without scoped .mdc files and a LEARNING_LOG.md entry the model reads on session start.

Log the decision once, enforce it forever

When you adopt Results team-wide, add one Learning Log line the persistence engine can re-hydrate:

## ADR-014 — Application errors are Results
- Handlers return Result<T> / ErrorOr<T>; no throw for business rules.
- API maps via Match / ToProblemDetails; controllers stay thin.
- Exceptions: infrastructure only (timeouts, corruption).

Next Monday, the model sees the ADR before it suggests throw new InvalidOperationException("duplicate") in a handler that has returned Results for six months.

Pair with the other failure modes

Result discipline does not replace DI lifetime audits (Scoped→Singleton capture) or hallucination breakers (seven-word stop phrase). It addresses a third failure mode: silent style regression — code that compiles, looks professional, and slowly erodes the conventions your team chose on purpose.


Preview boundary enforcement for free

arch-core-lite.mdc includes the "match existing Result patterns" guardrail among its architectural checks. Drop it into .cursor/rules/ and see fewer cross-layer and throw-based regressions today.

Download arch-core-lite.mdc →

Enforce the full error model across four rules.

Agentic Architect ships arch-core.mdc, dotnet-di.mdc, bug-breaker.mdc, and persistence.mdc — directory-scoped, with the Learning Log protocol. £19.99 one-time, lifetime updates, 14-day refund.

Get Agentic Architect — £19.99 →