Architecture and Core Concepts
This guide explains why Pragmatic.Result exists, how its pieces fit together, and how to choose the right type for each situation. Read this before diving into the API reference or integration guides.
The Problem
Section titled “The Problem”Most .NET applications signal failure by throwing exceptions. This creates three cascading problems that get worse as codebases grow.
Exceptions hide control flow
Section titled “Exceptions hide control flow”public async Task<User> GetUserAsync(int id){ var user = await _repository.FindByIdAsync(id); if (user is null) throw new NotFoundException($"User {id} not found");
if (!user.IsActive) throw new BusinessRuleException("User is inactive");
return user;}Reading this method, you see it returns User. But it can also throw two different exceptions. Nothing in the signature tells you this. The caller has to read the implementation (or hope for documentation) to know what to catch.
Exceptions are expensive for expected outcomes
Section titled “Exceptions are expensive for expected outcomes”“User not found” is not exceptional — it is a normal, expected business outcome. Yet the CLR allocates a full Exception object, captures a stack trace, and unwinds the call stack. On a hot API endpoint serving thousands of requests, this overhead is measurable.
// Every 404 allocates an Exception with a full stack tracetry{ var user = await _userService.GetUserAsync(id); return Ok(user);}catch (NotFoundException ex){ return NotFound(new ProblemDetails { Detail = ex.Message });}catch (BusinessRuleException ex){ return UnprocessableEntity(new ProblemDetails { Detail = ex.Message });}Catch blocks are incomplete by default
Section titled “Catch blocks are incomplete by default”Nothing forces the caller to handle all failure modes. Miss a catch block and the exception propagates to a global handler, producing a generic 500 instead of a meaningful error response. The compiler cannot help you because exceptions are not part of the type system.
// Forgot to catch BusinessRuleException// Result: 500 Internal Server Error instead of 422try{ var user = await _userService.GetUserAsync(id); return Ok(user);}catch (NotFoundException ex){ return NotFound(new ProblemDetails { Detail = ex.Message });}// BusinessRuleException? Unhandled. Boom: 500.The fundamental issue: exceptions are invisible in signatures, expensive for expected outcomes, and the compiler cannot enforce exhaustive handling.
The Solution
Section titled “The Solution”Pragmatic.Result makes error paths visible in the type system. A method that can fail returns a Result<TValue, TError> — a discriminated union that is either a success with a value or a failure with a typed error. The compiler enforces that you handle both cases.
The same “get user” operation:
public async Task<Result<User, NotFoundError>> GetUserAsync(int id){ var user = await _repository.FindByIdAsync(id); if (user is null) return NotFoundError.Create("User", id);
return user; // Implicit conversion to Result}The return type Result<User, NotFoundError> tells the caller everything:
- This method returns a
Useron success - It can fail with a
NotFoundError - No exceptions to guess about
The caller is forced to handle both paths:
var result = await _userService.GetUserAsync(id);
// Option 1: Pattern matching (exhaustive)var response = result.Match( user => Ok(user), error => NotFound(error));
// Option 2: Check-then-accessif (result.IsSuccess) return Ok(result.Value);return NotFound(result.Error);
// Option 3: TryGet patternif (result.TryGetValue(out var user)) return Ok(user);What you get:
- Type-safe errors — errors are part of the method signature, not hidden in
throwsdocumentation - Exhaustive handling —
Match()requires a handler for every error type, enforced at compile time - Zero allocation — all Result types are
readonly struct; the wrapper itself never allocates - HTTP-ready — built-in error types map directly to HTTP status codes and RFC 7807 ProblemDetails
- Railway-oriented programming —
Map,Bind,Tap,OrElsefor composing pipelines
How It Works: The Discriminated Union
Section titled “How It Works: The Discriminated Union”Result<TValue, TError> is a readonly struct with two private fields and a boolean discriminator. At any point in time, exactly one of the two fields holds a meaningful value.
Result<User, NotFoundError>┌──────────────────────────────────┐│ IsSuccess = true ││ _value = User { Id = 42 } │ ← Only this is populated│ _error = default │└──────────────────────────────────┘
Result<User, NotFoundError>┌──────────────────────────────────┐│ IsSuccess = false ││ _value = default ││ _error = NotFoundError { } │ ← Only this is populated└──────────────────────────────────┘Construction
Section titled “Construction”You never call a constructor directly. Results are created through factory methods or implicit conversions:
// Factory methods (explicit)var success = Result<User, NotFoundError>.Success(user);var failure = Result<User, NotFoundError>.Failure(NotFoundError.Create("User", id));
// Implicit conversions (preferred -- less ceremony)Result<User, NotFoundError> result = user; // SuccessResult<User, NotFoundError> result = NotFoundError.Create("User", id); // FailureThe implicit conversion from TValue creates a success. The implicit conversion from TError creates a failure. This is why you can write return user; or return new NotFoundError(...) directly in methods that return Result<User, NotFoundError>.
Accessing the value
Section titled “Accessing the value”Three patterns, from safest to most direct:
// 1. Match -- exhaustive, compiler-enforcedstring message = result.Match( user => $"Found: {user.Name}", error => $"Not found: {error.EntityId}");
// 2. TryGet -- safe, no exceptionsif (result.TryGetValue(out var user)) Console.WriteLine(user.Name);
// 3. Direct access -- throws if wrong stateif (result.IsSuccess){ var user = result.Value; // Safe after guard}Accessing .Value on a failure throws InvalidOperationException. Accessing .Error on a success throws InvalidOperationException. The analyzer PRAG0001 warns when you access .Value without a guard check.
Deconstruction
Section titled “Deconstruction”Results support C# deconstruction for terse code:
var (isSuccess, value, error) = result;if (isSuccess) Console.WriteLine(value!.Name);
// Two-element deconstructionvar (ok, user) = result;The Type Family
Section titled “The Type Family”Pragmatic.Result provides four core types, each designed for a specific use case.
Result<TValue, TError>
Section titled “Result<TValue, TError>”The primary type. Returns a value on success or a single typed error on failure.
Result<User, NotFoundError> GetUser(int id);Result<Order, ConflictError> PlaceOrder(OrderRequest request);When to use: The operation has one logical failure mode, or all failures map to the same error type.
Result<TValue, TError1, TError2, …>
Section titled “Result<TValue, TError1, TError2, …>”Multi-error variants generated by the source generator. Support 2 to 8 error types. Each error type gets its own Match parameter, enforcing exhaustive handling.
Result<User, NotFoundError, ForbiddenError> GetUser(int id, ClaimsPrincipal user);
var response = result.Match( user => Ok(user), notFound => NotFound(notFound), // Must handle NotFoundError forbidden => Forbid()); // Must handle ForbiddenErrorThe SG generates these variants as readonly struct types with a byte discriminator index. They live in Pragmatic.Result namespace and are available automatically when you reference the package.
When to use: The operation can fail with different error types that have different HTTP status codes or different semantic meaning to the caller.
VoidResult<TError>
Section titled “VoidResult<TError>”For operations that return nothing on success but can fail with a typed error.
VoidResult<ValidationError> ValidateAge(int age){ if (age < 0) return new ValidationError("Age cannot be negative"); return VoidResult<ValidationError>.Success();}
// In HTTP context: success = 204 No Content, failure = error status codeVoidResult<TError> has no .Value property. It has IsSuccess, IsFailure, .Error, TryGetError(), and Match().
When to use: DELETE operations, validation-only methods, fire-and-forget commands, any operation where success means “it worked, nothing to return.”
Maybe<T>
Section titled “Maybe<T>”For optional values where absence is normal, not an error.
Maybe<string> FindSetting(string key){ return settings.TryGetValue(key, out var value) ? Maybe<string>.Some(value) : Maybe<string>.None();}
var theme = FindSetting("theme").GetValueOrDefault("light");When to use: Cache lookups, optional configuration, TryParse patterns, dictionary lookups — any situation where “not found” is a normal outcome rather than an error condition.
Result<TValue> (untyped error)
Section titled “Result<TValue> (untyped error)”A convenience variant where the error type is the Error base class. Useful when you do not need typed error discrimination.
Result<User> GetUser(int id); // Error is Error base classWhen to use: Prototyping, or when the caller does not need to distinguish between error types.
Choosing the Right Type
Section titled “Choosing the Right Type”| Question | Type |
|---|---|
| Operation returns a value, one failure mode? | Result<T, TError> |
| Operation returns a value, multiple distinct failure modes? | Result<T, TError1, TError2, ...> |
| Operation returns nothing on success, can fail? | VoidResult<TError> |
| Value is optional, absence is normal? | Maybe<T> |
| Need aggregate errors from parallel operations? | Result<T[], AggregateError> via CollectAll |
Maybe vs Result
Section titled “Maybe vs Result”The distinction is semantic:
| Scenario | Use | Why |
|---|---|---|
| Cache lookup | Maybe<T> | Cache miss is normal, not an error |
| Database entity by ID | Result<T, NotFoundError> | 404 is meaningful to the API caller |
| Optional config value | Maybe<T> | Missing config = use default |
| Required external service call | Result<T, DependencyError> | Failure needs context (service name, retry) |
| Dictionary TryGetValue | Maybe<T> | Key absence is expected |
Rule of thumb: If the caller needs to know why the value is absent, use Result. If absence is just “nothing there,” use Maybe.
The Error Type System
Section titled “The Error Type System”All errors implement IError, a minimal interface with a single property:
public interface IError{ string Code { get; } // "NOT_FOUND", "VALIDATION_ERROR", etc.}The framework provides a richer base class Error that adds HTTP status codes, localization support, transient error detection, and retry hints:
public abstract record Error : IError{ public abstract string Code { get; } // "NOT_FOUND" public abstract int StatusCode { get; } // 404 public virtual string Title => string.Empty; // "Not Found" public virtual string MessageKey => ...; // "error.not.found" (for localization) public virtual bool IsTransient => false; // true for retryable errors public virtual TimeSpan? RetryAfter { get; } // Suggested retry delay public virtual IReadOnlyDictionary<string, object>? Parameters { get; }}Choosing IError vs Error
Section titled “Choosing IError vs Error”| Need | Implement | Example |
|---|---|---|
| Minimal error with just code + status | IError | record InsufficientFundsError : IError |
| HTTP status, localization, retry support | Error (inherit) | record RateLimitError : Error |
| Standard HTTP error (404, 409, etc.) | Use built-in types | NotFoundError, ConflictError |
Built-in HTTP Error Types
Section titled “Built-in HTTP Error Types”All built-in errors inherit from Error and live in the Pragmatic.Result.Http namespace.
| Error Type | HTTP Status | Code | Typical Use |
|---|---|---|---|
BadRequestError | 400 | BAD_REQUEST | Malformed request, missing headers |
UnauthorizedError | 401 | UNAUTHORIZED | Missing or invalid authentication |
ForbiddenError | 403 | FORBIDDEN | Authenticated but lacking permission |
NotFoundError | 404 | NOT_FOUND | Entity not found |
ConflictError | 409 | CONFLICT | Duplicate, concurrency conflict |
BusinessRuleError | 422 | BUSINESS_RULE_VIOLATION | Business rule prevents processing |
InternalServerError | 500 | INTERNAL_ERROR | Unhandled exception |
DependencyError | 502/503/504 | DEPENDENCY_ERROR | External service failure |
Each type provides semantic factory methods:
// NotFoundErrorNotFoundError.Create("User", userId);NotFoundError.For<User>(userId);NotFoundError.For<User, Guid>(userId);
// ConflictErrorConflictError.AlreadyExists("User", email);ConflictError.ConcurrencyConflict("Order", orderId);ConflictError.DuplicateKey("Email", email);
// BusinessRuleErrorBusinessRuleError.InsufficientFunds(requested: 100m, available: 50m);BusinessRuleError.LimitExceeded("order_items", limit: 10, requested: 15);BusinessRuleError.Inactive("Account");
// DependencyErrorDependencyError.Unavailable("PaymentService", retryAfter: TimeSpan.FromSeconds(30));DependencyError.Timeout("InventoryService");DependencyError.InvalidResponse("OrderService");
// UnauthorizedErrorUnauthorizedError.InvalidCredentials();UnauthorizedError.ExpiredToken();UnauthorizedError.MissingToken();
// ForbiddenErrorForbiddenError.MissingPermission("orders.write", resource: "orders/123");ForbiddenError.ActionDenied("delete", resource: "invoices");
// BadRequestErrorBadRequestError.MalformedJson();BadRequestError.MissingHeader("X-Api-Key");
// InternalServerErrorInternalServerError.From(exception, includeDetails: env.IsDevelopment());Custom Error Types
Section titled “Custom Error Types”Lightweight custom error (implement IError directly):
public record InsufficientFundsError(decimal Requested, decimal Available) : IError{ public string Code => "INSUFFICIENT_FUNDS"; public int StatusCode => 422;}Rich custom error (inherit from Error):
public sealed record RateLimitError : Error{ public override string Code => "RATE_LIMITED"; public override int StatusCode => 429; public override string Title => "Too Many Requests"; public override bool IsTransient => true; public override TimeSpan? RetryAfter { get; init; } = TimeSpan.FromSeconds(60);}AggregateError
Section titled “AggregateError”AggregateError collects multiple errors from parallel operations. It inherits from Error and contains a list of IError instances.
// From parallel resultsvar (userResult, orderResult) = await (GetUser(id), GetOrder(orderId));var aggregate = AggregateError.From(userResult, orderResult);if (aggregate is not null) return aggregate; // Contains errors from both operations
// From a collection of resultsvar allOrNothing = results.CollectAll();// All success -> Success(T[])// Any failure -> Failure(AggregateError with all failures)The StatusCode of an AggregateError is the highest status code among its contained errors (5xx outranks 4xx). IsTransient is true only if all contained errors are transient.
Implicit Conversions
Section titled “Implicit Conversions”Implicit conversions reduce ceremony. Understanding which conversions exist prevents confusion.
Result<TValue, TError>
Section titled “Result<TValue, TError>”| From | To | Direction |
|---|---|---|
TValue | Result<TValue, TError> | Value to success |
TError | Result<TValue, TError> | Error to failure |
Result<TValue, TError> | TValue | Success to value (throws on failure) |
// Value -> Success (safe, always works)Result<User, NotFoundError> result = user;
// Error -> Failure (safe, always works)Result<User, NotFoundError> result = NotFoundError.Create("User", id);
// Result -> Value (UNSAFE: throws InvalidOperationException if failure)User user = result; // Only safe after IsSuccess checkVoidResult<TError>
Section titled “VoidResult<TError>”| From | To | Direction |
|---|---|---|
TError | VoidResult<TError> | Error to failure |
VoidResult<TError> | bool | Result to boolean |
// Error -> FailureVoidResult<ValidationError> result = new ValidationError("Invalid");
// VoidResult -> bool (true if success)if (result) { /* success */ }Maybe<T>
Section titled “Maybe<T>”| From | To | Direction |
|---|---|---|
T | Maybe<T> | Value to Some |
Maybe<T> | bool | Option to boolean |
// Value -> SomeMaybe<string> option = "hello";
// Maybe -> bool (true if has value)if (option) { /* has value */ }Pattern Matching
Section titled “Pattern Matching”Match() is the primary way to consume Result types. It enforces exhaustive handling — the compiler requires a handler for every possible outcome.
Single-error Result
Section titled “Single-error Result”Result<User, NotFoundError> result = ...;
// Functional: returns a valuestring message = result.Match( user => $"Found: {user.Name}", error => $"Not found: {error.EntityId}");
// Imperative: executes side effectsresult.Match( user => Console.WriteLine(user.Name), error => logger.LogWarning("User {Id} not found", error.EntityId));Multi-error Result
Section titled “Multi-error Result”Each error type gets its own handler:
Result<Order, NotFoundError, ForbiddenError, ConflictError> result = ...;
var response = result.Match( order => Ok(order), notFound => NotFound(), forbidden => Forbid(), conflict => Conflict());// Removing any handler is a compile errorVoidResult
Section titled “VoidResult”No success value parameter — just a parameterless function:
VoidResult<ValidationError> result = ...;
string message = result.Match( () => "Valid", error => $"Invalid: {error.Message}");Maybe<string> option = ...;
string display = option.Match( value => $"Got: {value}", () => "Nothing");Railway-Oriented Programming
Section titled “Railway-Oriented Programming”Map, Bind, and their async variants let you compose operations without nested if-else checks. If any step fails, subsequent steps are skipped and the error propagates.
Map — transform the success value
Section titled “Map — transform the success value”Result<string, NotFoundError> result = GetUsername(id);
// Map transforms the value if success, passes through the error if failureResult<int, NotFoundError> length = result.Map(name => name.Length);Bind — chain Result-returning operations
Section titled “Bind — chain Result-returning operations”Result<User, NotFoundError> user = GetUser(id);
// Bind chains another Result-returning operationResult<Order, NotFoundError> order = user.Bind(u => GetLatestOrder(u.Id));The difference: Map takes Func<T, TNew> (value in, value out). Bind takes Func<T, Result<TNew, TError>> (value in, Result out). Use Bind when the next step can also fail.
MapError — transform the error
Section titled “MapError — transform the error”Result<User, NotFoundError> result = GetUser(id);
// Convert error type for a different contextResult<User, ApiError> mapped = result.MapError(e => new ApiError(e.Code));Async pipeline
Section titled “Async pipeline”Chain async operations in a single expression:
var result = await userService.GetByIdAsync(userId) .MapAsync(user => enrichmentService.EnrichAsync(user)) .BindAsync(user => authService.CheckAccessAsync(user)) .TapAsync(user => auditService.LogAccessAsync(user)) .OnFailureAsync(error => alertService.NotifyAsync(error));Each method in the chain:
| Method | Purpose | Signature |
|---|---|---|
MapAsync | Transform success value (async) | Func<T, Task<TNew>> |
BindAsync | Chain another Result-returning async operation | Func<T, Task<Result<TNew, TError>>> |
TapAsync | Side effect on success (logging, metrics) | Func<T, Task> |
OnFailureAsync | Side effect on failure | Func<TError, Task> |
EnsureAsync | Validate with async predicate, fail if false | Func<T, Task<bool>> |
OrElseAsync | Recover from failure with fallback | Func<TError, Task<Result<T, TError>>> |
MatchAsync | Extract final value from async chain | Both handlers |
Factory methods for bridging
Section titled “Factory methods for bridging”Convert existing patterns into Results:
// Nullable -> ResultUser? user = await repository.FindByIdAsync(id);var result = Result.FromNullable(user, NotFoundError.Create("User", id));
// Lazy error (factory only called on null)var result = Result.FromNullable(user, () => NotFoundError.For<User>(id));
// Exception-throwing code -> Resultvar result = Result.Try( () => JsonSerializer.Deserialize<Order>(json)!, ex => InternalServerError.From(ex));
// Async variantvar result = await Result.TryAsync( async ct => await httpClient.GetFromJsonAsync<Order>(url, ct), ex => DependencyError.Unavailable("OrderService"), cancellationToken);Collection operations
Section titled “Collection operations”Work with collections of Results:
Result<User, NotFoundError>[] results = await Task.WhenAll( ids.Select(id => userService.GetByIdAsync(id)));
// Extract successes onlyIEnumerable<User> users = results.GetSuccesses();
// Extract failures onlyIEnumerable<NotFoundError> errors = results.GetFailures();
// Split into bothvar (users, errors) = results.Partition();
// All-or-nothing: all succeed or aggregate errorsvar allOrNothing = results.CollectAll();// Success -> Result<User[], AggregateError> with all users// Failure -> Result<User[], AggregateError> with AggregateError containing all failuresWhat Gets Generated
Section titled “What Gets Generated”The Pragmatic.Result source generator produces two categories of output.
Multi-error Result variants
Section titled “Multi-error Result variants”The SG generates Result<TValue, TError1, TError2> through Result<TValue, TError1, ..., TError8> as readonly struct types. These are generated once and included in the package. Each variant has:
- A
byte _indexdiscriminator (0 = success, 1-N = error type index) - Separate fields for each error type (no boxing)
Match()with N+1 parameters (success + one per error type)TryGetValue()andTryGetError1()throughTryGetErrorN()- Implicit conversions from
TValueand eachTError IResultBaseimplementation for ASP.NET Core integration
The same pattern applies to VoidResult<TError1, TError2> through VoidResult<TError1, ..., TError8>.
Custom Result types via [GenerateResult]
Section titled “Custom Result types via [GenerateResult]”When you define a domain-specific combination of error types used across multiple methods, [GenerateResult] generates a named Result type:
[GenerateResult(typeof(NotFoundError), typeof(ValidationError), typeof(ConflictError))]public interface IUserOperationResult { }
// SG generates: UserOperationResult<TValue>// - Same features as Result<TValue, NotFoundError, ValidationError, ConflictError>// - Named type for better readability in method signaturesGenerated files appear under obj/Debug/net10.0/generated/ in the IDE and are fully debuggable.
Ecosystem Integration
Section titled “Ecosystem Integration”Pragmatic.Result is the error handling foundation for the entire Pragmatic ecosystem. Other modules consume and produce Result types.
ASP.NET Core (Pragmatic.Result.AspNetCore)
Section titled “ASP.NET Core (Pragmatic.Result.AspNetCore)”Automatic conversion from Result types to HTTP responses:
- Minimal APIs:
WithResultHandling()on route groups convertsResult<T, E>to200 OKorProblemDetailswith the error’s status code - Controllers:
ResultActionFilterdoes the same for controller actions returning Result types - VoidResult maps to
204 No Contenton success - ProblemDetails follow RFC 7807, with error-specific extensions (
entityType,entityId, etc.)
Entity Framework Core (Pragmatic.Result.EntityFrameworkCore)
Section titled “Entity Framework Core (Pragmatic.Result.EntityFrameworkCore)”Query extensions that return Result types instead of null:
var result = await dbContext.Users.FindAsResultAsync<User, int>(id);// Returns Result<User, NotFoundError> instead of User?Pragmatic.Endpoints
Section titled “Pragmatic.Endpoints”Endpoints declare error types in their base class generics. The SG generates HTTP status mapping and OpenAPI response schemas automatically:
[Endpoint(HttpVerb.Get, "/users/{id}")]public partial class GetUser : Endpoint<UserDto, NotFoundError>{ // SG generates: 200 OK (UserDto) + 404 Not Found (ProblemDetails)}Pragmatic.Actions
Section titled “Pragmatic.Actions”DomainAction<T> and Mutation<T> use Result<T, IError> as their return type. The invoker pipeline handles error propagation through filters, validators, and processors.
Pragmatic.Validation
Section titled “Pragmatic.Validation”ValidationError implements IError and is the standard error for input validation failures. The validation SG generates validators that return VoidResult<ValidationError>.
Pragmatic.Persistence
Section titled “Pragmatic.Persistence”Repository methods return Result<T, NotFoundError>. Bulk operations use AggregateError for partial failure scenarios. SaveChangesAsResultAsync (from Pragmatic.Result.EFCore) converts DbUpdateException into typed database errors (DbConflictError, DbConstraintError, etc.).
JSON Serialization
Section titled “JSON Serialization”Result types include built-in System.Text.Json converters via ResultJsonConverterFactory.
Registration
Section titled “Registration”// ASP.NET Corebuilder.Services.ConfigureHttpJsonOptions(options =>{ options.SerializerOptions.Converters.Add(new ResultJsonConverterFactory());});Wire format
Section titled “Wire format”// Result<T, E> -- success{ "isSuccess": true, "value": { "id": 1, "name": "Alice" } }
// Result<T, E> -- failure{ "isSuccess": false, "error": { "code": "NOT_FOUND", "statusCode": 404 } }
// VoidResult<E> -- success{ "isSuccess": true }
// VoidResult<E> -- failure{ "isSuccess": false, "error": { "code": "VALIDATION_ERROR" } }
// Maybe<T> -- some{ "hasValue": true, "value": "cached-data" }
// Maybe<T> -- nonenullPerformance
Section titled “Performance”All Result types are readonly struct and designed for zero allocation on the hot path.
| Design decision | Impact |
|---|---|
readonly struct for all types | No heap allocation for the Result wrapper |
[MethodImpl(AggressiveInlining)] on hot methods | JIT inlines IsSuccess, Value, Match, TryGetValue |
| Private constructors + static factories | Prevents invalid states, enables optimization |
byte index for multi-error variants | Single byte discriminator instead of per-error bool fields |
[DoesNotReturn] on throw helpers | JIT eliminates dead code paths after guards |
What allocates
Section titled “What allocates”- The Result struct itself: Never. It lives on the stack or inline in the containing object.
- The payload (TValue, TError): Only if the type is a reference type (
class,record class). Usingreadonly structorrecord structfor errors makes the entire operation allocation-free. Matchwith lambdas: May allocate a delegate if the lambda captures local variables. For zero-allocationMatch, useTryGetValue/TryGetErroror static lambdas.
See Also
Section titled “See Also”- Getting Started — Install and create your first Result in 5 minutes
- API Reference — Complete method documentation
- Common Mistakes — Pitfalls and how to avoid them
- Troubleshooting — Problem/solution guide with diagnostics reference
- Migration Guide — Migrate from exceptions or other libraries
- Localization — Localize error messages with IErrorMessageResolver