error-handling
Skill5 starsChanged 6 days agoMITView on GitHub
The canonical Result<TValue, TError> type, typed error records, ProblemDetails (RFC 9457), and global exception boundaries for .NET APIs.
80 other projects use a version of this.
Adopted by
Most recently taken up by
- codewithmukesh/dotnet-claude-kittheir own version, first seen Nov 8, 2026
- lhbsaa/apex-unifiedtheir own version, first seen Nov 8, 2026
- ahmadvahidi/skill-atlastheir own version, first seen Nov 8, 2026
- nisarth/claude-skills-storetheir own version, first seen Nov 8, 2026
- AndersonAndrad/skillstheir own version, first seen Nov 8, 2026
- AnyOrgName/php-skillstheir own version, first seen Nov 8, 2026
Used with
Projects with it also use dependency-injection, type-safety and 3 more.
Other files people use alongside this one.
Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
HamzaSheikh768Skilldependency-injection
Leverages FastAPI's powerful Depends system for reusable authentication, database sessions, rate limiting, and business logic injection.
Also in 5 projects that use this
MANVENDRA-githubCursor ruletype-safety
Python static typing discipline — annotate every signature, run a strict type checker, treat escape hatches as code smells. Apply when working in any Python file. Skip for throwaway scripts and notebooks where the code will not outlive the session.
Also in 3 projects that use this
junhjangSkillconfiguration
Adding a new config file, adding a field, or wiring a config consumer. Enforces schema validation at startup (no permissive defaults), env-var indirection for secrets, layered config (defaults / per-env / operator-override), hot-reload explicit per field.
Also in 4 projects that use this
dbtlrSkilldependencies
Use when adding, evaluating, or updating Rust crate dependencies. Also use when considering whether to implement something yourself vs pulling in a library.
Also in 4 projects that use this
OpenAISkillauth
Authentication integration guidance — Clerk (native Vercel Marketplace), Descope, and Auth0 setup for Next.js applications. Covers middleware auth patterns, sign-in/sign-up flows, and Marketplace provisioning. Use when implementing user authentication.
Also in 5 projects that use this
Other versions80 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 80
- microsoft/data-formulator★ 17ksame name
- epicenter-md/epicenter★ 4.8ksame name
- EpicenterHQ/epicenter★ 4.8ksame name
- software-mansion/react-native-executorch★ 1.8ksame name
- CloudAI-X/claude-workflow★ 1.4ksame name
- CloudAI-X/claude-workflow-v2★ 1.4ksame name
- codewithmukesh/dotnet-claude-kit★ 808same name
- alchemyplatform/aa-sdk★ 321same name
- Resgrid/Core★ 238same name
- DevelopersGlobal/ai-agent-skills★ 216same name
70 more not listed
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository15 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- aspnet-api-patternsSkill
- authenticationSkill
- cachingSkill
- clean-architectureSkill
- convention-learnerSkill
- dddSkill
- dotnet-project-initSkill
- ef-core-patternsSkill
- loggingSkill
- modern-csharpSkill
- opentelemetrySkill
- resilienceSkill
- testing-dotnetSkill
- vertical-sliceSkill
The file
We split this skill into the rules it gives an agent
3 rules. 0 are common, 1 is distinctive, and 2 are very specific to this project.
0
Common
Most of the 64 edited versions we read keep these.
1
Distinctive
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 21
Never throw exceptions for expected domain outcomes.
- Very specific: line 139
Leave type unset. AddProblemDetails() fills type and title with the RFC 9110 section URI for the status code (e.g.
- Very specific: line 201
Never put exception.Message or the stack trace in detail, because it reaches the client.
“Common” is measured: kept by at least 6 in 10 of the 64 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Error Handling Patterns
This skill owns the Result<TValue, TError> definition. ddd, clean-architecture and
vertical-slice use it and do not redefine it.
Philosophy: When to Use Results vs Exceptions
| Scenario | Approach |
| Domain rule violation (not found, invalid state, business constraint) | Result<TValue, TError> — expected failure path |
| Infrastructure failure (DB timeout, network error, config missing) | Exception — unexpected, unrecoverable at call site |
| Validation failure (bad input from HTTP layer) | ValidationProblem via model binding / FluentValidation |
| Unhandled exception escaping to HTTP | GlobalExceptionHandler → 500 ProblemDetails |
Never throw exceptions for expected domain outcomes. Never swallow exceptions at call sites.
Result Type
Define once in the Domain project (e.g. Domain/Common/Result.cs). It has no dependencies, so every
layer can reference it:
namespace MyApp.Domain.Common;
public readonly record struct Result<TValue, TError>
{
private readonly TValue? _value;
private readonly TError? _error;
public bool IsSuccess { get; }
public bool IsFailure => !IsSuccess;
public TValue Value => IsSuccess ? _value! : throw new InvalidOperationException("Result is a failure; read Error.");
public TError Error => IsFailure ? _error! : throw new InvalidOperationException("Result is a success; read Value.");
private Result(TValue value) { _value = value; IsSuccess = true; }
private Result(TError error) { _error = error; IsSuccess = false; }
public static Result<TValue, TError> Success(TValue value) => new(value);
public static Result<TValue, TError> Failure(TError error) => new(error);
public static implicit operator Result<TValue, TError>(TValue value) => Success(value);
public static implicit operator Result<TValue, TError>(TError error) => Failure(error);
public TResult Match<TResult>(Func<TValue, TResult> onSuccess, Func<TError, TResult> onFailure)
=> IsSuccess ? onSuccess(_value!) : onFailure(_error!);
}
// Success marker for operations that return no value: Result<Done, TError>.
public readonly record struct Done
{
public static readonly Done Value = default;
}Gotchas:
default(Result<,>), including an uninitialised field of this type, is a failure with a nullError. Always construct one throughSuccess/Failureor the implicit conversions.- The implicit conversions let a method
return order;orreturn new NotFoundError(...). They become ambiguous whenTValueandTErrorare the same type or related by inheritance, and C# applies no user-defined conversion from an interface-typed value. CallSuccess/Failureexplicitly in those cases. - The no-value form is named
Done, notUnit(clashes withMediatR.Unit) orOk(clashes withMicrosoft.AspNetCore.Http.HttpResults.Ok). - A discarded
Resultcompiles silently. Treat one as a bug in review.
Typed Error Records
namespace MyApp.Domain.Errors;
public abstract record DomainError(string Message);
public sealed record NotFoundError(string Resource, object Id)
: DomainError($"{Resource} with id '{Id}' was not found.");
public sealed record ConflictError(string Resource, string Reason)
: DomainError($"{Resource} conflict: {Reason}");
public sealed record ValidationError(string Field, string Reason)
: DomainError($"Validation failed for '{Field}': {Reason}");
public sealed record ForbiddenError(string Action, string Resource)
: DomainError($"Not permitted to {Action} {Resource}.");Returning Results
The aggregate owns the business rule and returns a result. The service propagates it rather than re-checking state:
public class OrderService(IOrderRepository repo, TimeProvider clock)
{
public async Task<Result<Order, DomainError>> CancelAsync(Guid id, CancellationToken ct)
{
var order = await repo.GetByIdAsync(id, ct);
if (order is null)
return new NotFoundError("Order", id);
var cancelled = order.Cancel(clock); // Result<Done, DomainError>
if (cancelled.IsFailure)
return cancelled.Error;
await repo.SaveChangesAsync(ct);
return order;
}
}Mapping Results to HTTP (Minimal API)
app.MapPost("/orders/{id:guid}/cancel", async (Guid id, OrderService svc, CancellationToken ct) =>
{
var result = await svc.CancelAsync(id, ct);
return result.Match<IResult>(order => TypedResults.Ok(order), MapError);
});
static IResult MapError(DomainError error) => error switch
{
NotFoundError e => TypedResults.Problem(e.Message, statusCode: StatusCodes.Status404NotFound),
ConflictError e => TypedResults.Problem(e.Message, statusCode: StatusCodes.Status409Conflict),
ForbiddenError e => TypedResults.Problem(e.Message, statusCode: StatusCodes.Status403Forbidden),
ValidationError e => TypedResults.ValidationProblem(
new Dictionary<string, string[]> { [e.Field] = [e.Reason] }),
_ => TypedResults.Problem(error.Message, statusCode: StatusCodes.Status500InternalServerError)
};- A domain
ForbiddenErrormaps to a 403 problem, notTypedResults.Forbid().Forbid()runs the authentication handler's forbid flow, which for cookie auth redirects to an access-denied page. - Leave
typeunset.AddProblemDetails()fillstypeandtitlewith the RFC 9110 section URI for the status code (e.g. 404 →https://tools.ietf.org/html/rfc9110#section-15.5.5). - Returning
IResultloses OpenAPI response metadata. Declare.ProducesProblem(404)and so on for the endpoint, or return aResults<Ok<T>, ProblemHttpResult, ...>union.
Controller mapping: references/controllers-and-jobs.md.
ProblemDetails (RFC 9457)
RFC 9457 obsoletes RFC 7807. The JSON shape is unchanged.
builder.Services.AddProblemDetails(options =>
{
options.CustomizeProblemDetails = ctx =>
{
ctx.ProblemDetails.Instance =
$"{ctx.HttpContext.Request.Method} {ctx.HttpContext.Request.Path}";
ctx.ProblemDetails.Extensions["traceId"] =
Activity.Current?.Id ?? ctx.HttpContext.TraceIdentifier;
};
});| Field | Description | Example |
status | HTTP status code | 404 |
title | Short human-readable summary | "Not Found" |
type | URI identifying the problem type | "https://tools.ietf.org/html/rfc9110#section-15.5.5" |
detail | Specific explanation for this occurrence | "Order 42 was not found." |
instance | Reference to this occurrence | "GET /orders/42" |
Global Exception Handler
Catches unhandled exceptions at the HTTP boundary. Writing through IProblemDetailsService keeps
the CustomizeProblemDetails callback (traceId, instance) in the response:
public sealed class GlobalExceptionHandler(
IProblemDetailsService problemDetails,
ILogger<GlobalExceptionHandler> logger) : IExceptionHandler
{
public async ValueTask<bool> TryHandleAsync(HttpContext ctx, Exception exception, CancellationToken ct)
{
logger.LogError(exception, "Unhandled exception on {Method} {Path}", ctx.Request.Method, ctx.Request.Path);
ctx.Response.StatusCode = StatusCodes.Status500InternalServerError;
return await problemDetails.TryWriteAsync(new ProblemDetailsContext
{
HttpContext = ctx,
Exception = exception,
ProblemDetails = { Title = "An unexpected error occurred." }
});
}
}
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddProblemDetails();
app.UseExceptionHandler(); // first in the pipeline, before routingNever put exception.Message or the stack trace in detail, because it reaches the client.
Background-job exception boundaries: references/controllers-and-jobs.md.
Do / Don't
| Do | Don't |
Return Result<TValue, TError> for domain failures | Throw DomainException for expected failure paths |
| Define typed error records per domain concept | Use stringly-typed error messages |
| Catch exceptions only at HTTP/job boundaries | Catch Exception at the service or repository layer |
| Log at the boundary with full exception | Log the same exception multiple times as it propagates |
Use TypedResults (compile-time checked) over Results | Mix IActionResult and IResult in the same endpoint |
Include traceId in ProblemDetails extensions | Expose stack traces or internal exception messages to clients |
See Also
skills/modern-csharp/SKILL.md— record and pattern-matching gotchas used in error typesskills/aspnet-api-patterns/SKILL.md— endpoint conventions that consume these patterns
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

