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.*
Top comments (0)