dotnet-error-handling

Installation
SKILL.md

ASP.NET Core error handling

An API has exactly two ways to report that something went wrong, and they must never blur together:

  • Expected failures - validation is rejected, the row is not there, a uniqueness rule is broken. These are part of the contract, so they are return values: a Result / Result<T> or a closed error union the caller branches on.
  • Unexpected failures - a dependency is down, an invariant is violated, a bug throws. These are exceptions, and they are caught in exactly one place: a global handler.

The language-level call - when to throw versus when to return - is csharp. This skill is only about how a failure reaches the wire. Floor is .NET 8 / C# 12.

Model expected failures as return values

  • An application or domain operation that can fail in a foreseeable way returns its outcome instead of throwing. Two shapes both work; pick one per codebase and stay with it:
    • a Result<T> holding either a value or one-or-more errors (IsSuccess, Value, Errors);
    • a closed union - abstract record Error(string Code, string Message); with sealed record NotFound(...) : Error and friends - resolved by a switch expression.
  • Throwing to signal an ordinary outcome (not found, invalid input, conflict) is the thing to avoid: it is slower on the failure path, it hides the failure from the method signature, and it pushes a try/catch to every call site.

One place maps an error to a status

  • Convert the domain error to an HTTP status in a single helper - an Error -> IResult switch, or a result.Match(onOk, onError) extension - so an identical failure yields an identical status and body across every endpoint. Handlers call the helper; they do not each pick a status code.
  • House mapping, kept in one file: invalid input 400, unauthenticated 401, forbidden 403, missing resource 404, conflict/uniqueness 409, broken domain rule 422, anything unmapped 500.
Installs
12
GitHub Stars
1
First Seen
Jun 21, 2026
dotnet-error-handling — envoydev/claude-stack