DEV Community

Cover image for Teach Cursor Result<T> Instead of Throwing
agentic.standard.contact
agentic.standard.contact

Posted on • Originally published at agenticstandardcontact-byte.github.io

Teach Cursor Result<T> Instead of Throwing

Teach Cursor Result 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.

1. ****The prompt did not mention ErrorOr vs Result vs FluentResults — so it picks whichever type name it saw most recently in training.

1. ****There is no enforced rule on Application-layer files — only a memory in a chat you closed yesterday.



[](01-the-context-tax.html)
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).



``[](https://agenticstandardcontact-byte.github.io/agentic-architect/)
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:




```plaintext

```


## 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


[](02-scoped-singleton-di-bug.html)[](03-cursor-hallucination-loop-breaker.html)****
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.

---

*Originally published at [https://agenticstandardcontact-byte.github.io/agentic-architect/blog/04-cursor-result-not-throw.html](https://agenticstandardcontact-byte.github.io/agentic-architect/blog/04-cursor-result-not-throw.html). Part of the [Agentic Architect](https://agenticstandardcontact-byte.github.io/agentic-architect/) persistence kit for Cursor + .NET.*
Enter fullscreen mode Exit fullscreen mode

Top comments (0)