Troubleshooting
Practical problem/solution guide for Pragmatic.Result. Each section covers a common issue, the likely causes, and the fix.
InvalidOperationException: Cannot Access Value
Section titled “InvalidOperationException: Cannot Access Value”Your code throws InvalidOperationException with the message “Cannot access Value when result is failure.”
Checklist
Section titled “Checklist”-
Did you check
IsSuccessbefore accessing.Value? This is the most common cause. The Result is in a failure state, but you are reading the success value:// Throws if result is failurevar user = result.Value;// Safe: check firstif (result.IsSuccess)var user = result.Value; -
Use
TryGetValue()for the TryParse pattern:if (result.TryGetValue(out var user))Console.WriteLine(user.Name); -
Use
Match()for exhaustive handling:var name = result.Match(user => user.Name,error => "Unknown"); -
Check the PRAG0001 analyzer. The Roslyn analyzer warns at build time when
.Valueis accessed without a guard. If PRAG0001 is not firing, verify thatPragmatic.Result.Analyzersis referenced.
InvalidOperationException: Cannot Access Error
Section titled “InvalidOperationException: Cannot Access Error”Your code throws InvalidOperationException with the message “Cannot access Error when result is success.”
Checklist
Section titled “Checklist”-
Did you check
IsFailurebefore accessing.Error? The Result is in a success state, but you are reading the error:// Throws if result is successvar error = result.Error;// Safe: check firstif (result.IsFailure)Log(result.Error); -
Use
TryGetError()for safe access:if (result.TryGetError(out var error))logger.LogWarning("Failed: {Code}", error.Code);
Ambiguous Implicit Conversion (CS0121)
Section titled “Ambiguous Implicit Conversion (CS0121)”The compiler reports CS0121: The call is ambiguous between two implicit conversions.
Possible Causes
Section titled “Possible Causes”TValue and TError are the same type. If both type parameters resolve to the same type, the compiler cannot determine which implicit conversion to use:
// Ambiguous: string could be success value or error codeResult<string, StringError> result = "hello"; // Which conversion?Fix: Use the explicit factory method:
var result = Result<string, StringError>.Success("hello");var result = Result<string, StringError>.Failure(new StringError("oops"));TValue is assignable to TError (or vice versa). This happens when using base types like IError:
Result<IError, IError> result = someError; // AmbiguousFix: Avoid using the same type or overlapping types for TValue and TError. If unavoidable, use explicit factories.
Result Not Converting to ProblemDetails
Section titled “Result Not Converting to ProblemDetails”Your API returns raw Result JSON ({ "isSuccess": false, "error": {...} }) instead of ProblemDetails.
Checklist
Section titled “Checklist”-
For Minimal APIs: Did you add
WithResultHandling()to the route group?var api = app.MapGroup("").WithResultHandling(); -
For Controllers: Did you register
ResultActionFilter?builder.Services.AddControllers(options =>{options.Filters.Add<ResultActionFilter>();}); -
Did you register
AddPragmaticResult()?builder.Services.AddPragmaticResult(); -
Is
[SkipResultHandling]applied? This attribute explicitly opts out of automatic Result-to-HTTP conversion. Remove it if you want ProblemDetails. -
Are you using Pragmatic.Endpoints? When using
[Endpoint], Result-to-HTTP conversion is handled by the SG-generated handler. You do not needResultActionFilterorWithResultHandling()— the SG generates the mapping code directly.
Wrong HTTP Status Code Returned
Section titled “Wrong HTTP Status Code Returned”The API returns 422 instead of the expected status code (e.g., 404, 409).
Possible Causes
Section titled “Possible Causes”Custom error missing StatusCode. When an error implements only IError without a StatusCode property, the ASP.NET Core integration defaults to 422 for unknown error types:
// Missing StatusCode -- defaults to 422public record PaymentRequiredError : IError{ public string Code => "PAYMENT_REQUIRED";}
// Fixed: add StatusCodepublic record PaymentRequiredError : IError{ public string Code => "PAYMENT_REQUIRED"; public int StatusCode => 402;}Wrong error type returned. Verify the error instance you are returning. If you declare Result<T, NotFoundError> but return a different error type via implicit conversion from a shared method, the status code reflects the actual error type, not the declared type.
Built-in error with correct status. Verify the error type’s status code:
| Error Type | Status Code |
|---|---|
BadRequestError | 400 |
UnauthorizedError | 401 |
ForbiddenError | 403 |
NotFoundError | 404 |
ConflictError | 409 |
BusinessRuleError | 422 |
InternalServerError | 500 |
DependencyError | 502, 503, or 504 |
Multi-Error Result Types Not Available
Section titled “Multi-Error Result Types Not Available”You reference Result<T, E1, E2> but the compiler does not recognize the type.
Checklist
Section titled “Checklist”-
Is the Pragmatic.Result package referenced? The multi-error variants are generated by the package’s source generator. Verify the package reference in your
.csproj. -
Clean and rebuild. Source generator output can become stale. Run:
Terminal window dotnet cleandotnet build -
Check the SG output. In Visual Studio, expand Dependencies > Analyzers > Pragmatic.Result.SourceGenerator in Solution Explorer. You should see files like
Result3_Generated.g.csthroughResult9_Generated.g.cs. -
Maximum 8 error types. The SG generates variants up to
Result<TValue, TError1, ..., TError8>. If you need more than 8 error types, consider aggregating related errors into a single type or usingAggregateError.
Match Not Compiling (Wrong Number of Handlers)
Section titled “Match Not Compiling (Wrong Number of Handlers)”The call to Match() does not compile because the number of handler lambdas does not match the number of type parameters.
Match() requires exactly N+1 handlers: one for the success value, plus one per error type.
// Result<T, E1, E2> requires 3 handlers:result.Match( value => ..., // Success handler error1 => ..., // TError1 handler error2 => ...); // TError2 handler
// Result<T, E1> requires 2 handlers:result.Match( value => ..., // Success handler error => ...); // TError handler
// VoidResult<E> requires 2 handlers, but success has no parameter:result.Match( () => ..., // Success (no value) error => ...); // TError handlerIf you are using a multi-error Result and forgot a handler, the compiler error message will indicate which overload could not be resolved. Count the error type parameters and add the missing handler.
JSON Serialization Not Working
Section titled “JSON Serialization Not Working”Result types serialize as {} or throw JsonException.
Checklist
Section titled “Checklist”-
Register
ResultJsonConverterFactory:// For Minimal APIsbuilder.Services.ConfigureHttpJsonOptions(options =>{options.SerializerOptions.Converters.Add(new ResultJsonConverterFactory());});// For Controllersbuilder.Services.AddControllers().AddJsonOptions(options =>{options.JsonSerializerOptions.Converters.Add(new ResultJsonConverterFactory());});// For standalone serializationvar options = new JsonSerializerOptions();options.Converters.Add(new ResultJsonConverterFactory()); -
Check the JSON structure for deserialization. The expected wire format is:
// Success{ "isSuccess": true, "value": { ... } }// Failure{ "isSuccess": false, "error": { "code": "NOT_FOUND", ... } }If the
isSuccessproperty is missing, deserialization will fail. -
Custom error types must be serializable. Properties on your custom error must have public getters for
System.Text.Jsonto include them.
ProblemDetails Missing Error-Specific Fields
Section titled “ProblemDetails Missing Error-Specific Fields”The ProblemDetails response has type, title, status, and code but is missing fields like entityType, entityId, or reason.
Checklist
Section titled “Checklist”-
Use the factory methods on built-in errors. Fields like
EntityTypeandEntityIdare populated by the factory methods:// These populate ProblemDetails extensionsNotFoundError.Create("User", userId); // entityType + entityIdConflictError.AlreadyExists("User", email); // entityType + reasonBadRequestError.MissingHeader("X-Api-Key"); // field + reason -
For custom errors inheriting from
Error: Override theParametersproperty to include your custom fields in ProblemDetails:public sealed record OrderLimitError : Error{public override string Code => "ORDER_LIMIT_EXCEEDED";public override int StatusCode => 422;public int MaxItems { get; init; }public int RequestedItems { get; init; }public override IReadOnlyDictionary<string, object>? Parameters =>new Dictionary<string, object>{["maxItems"] = MaxItems,["requestedItems"] = RequestedItems};} -
For errors implementing
IErrordirectly: The ProblemDetails converter uses theCodeandStatusCodeproperties. Additional fields must be added via theParametersdictionary on theErrorbase class, or by implementing a customIProblemDetailsWriter.
High Allocations from Result Operations
Section titled “High Allocations from Result Operations”Profiling shows heap allocations where you expect zero-allocation behavior.
Common Causes
Section titled “Common Causes”-
Boxing. Casting
Result<T, E>toobjectorIResultBaseboxes the struct:// Allocates: boxingobject obj = result;IResultBase baseResult = result;// No allocation: use concrete typevar result = GetUser(id); -
Lambda closures. Lambdas that capture local variables allocate a closure object:
// Allocates: captures 'id'var id = GetId();result.Map(x => ProcessWith(x, id));// No allocation: static lambda or method groupresult.Map(static x => x.ToString());result.Map(Process); -
LINQ in hot paths. Methods like
results.GetSuccesses()use LINQ internally. For hot paths, use manual iteration:// LINQ (allocates enumerator)var users = results.GetSuccesses().ToList();// Manual (no LINQ allocation)var users = new List<User>(results.Length);foreach (var r in results){if (r.TryGetValue(out var user))users.Add(user);} -
Reference-type errors. If your error type is a
record class(the default for records), the error itself allocates on the heap. For zero-allocation errors on failure paths, userecord struct:// Allocates: record class (default)public record NotFoundError : Error { ... }// Zero-allocation: record structpublic readonly record struct NotFoundError : IError { ... }Note: the built-in errors (
NotFoundError,ConflictError, etc.) arerecord classtypes because they inherit from theErrorabstract record. This is acceptable because failure paths are not the hot path.
Diagnostics Reference
Section titled “Diagnostics Reference”| ID | Severity | Cause | Fix |
|---|---|---|---|
| PRAG0001 | Warning | Unsafe .Value access without IsSuccess/IsFailure check | Add guard check, use TryGetValue(), or use Match() |
The PRAG0001 analyzer is included in the Pragmatic.Result.Analyzers package. It detects direct access to .Value or .Error without a preceding state check in the same scope. Safe patterns that suppress the warning:
// Guard with ifif (result.IsSuccess) var value = result.Value;
// Guard with ternaryvar value = result.IsSuccess ? result.Value : default;
// Guard-return patternif (!result.IsSuccess) return;var value = result.Value;
// Match (no .Value access needed)var value = result.Match(v => v, e => default);Can I use Result with async methods?
Section titled “Can I use Result with async methods?”Yes. Wrap the Result in a Task:
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;}Async extension methods (MapAsync, BindAsync, TapAsync) work on Task<Result<T, E>> for pipeline composition.
How do I create a custom [GenerateResult] type?
Section titled “How do I create a custom [GenerateResult] type?”Define an interface with the [GenerateResult] attribute listing your error types:
[GenerateResult(typeof(NotFoundError), typeof(ValidationError), typeof(ConflictError))]public interface IUserOperationResult { }The SG generates UserOperationResult<TValue> with implicit conversions, Match(), and all standard Result features.
How do I handle AggregateError?
Section titled “How do I handle AggregateError?”AggregateError.Errors gives you the list of individual errors:
if (result.TryGetError(out var aggregate)){ foreach (var error in aggregate.Errors) { logger.LogWarning("Sub-error: {Code}", error.Code); }}Is Result compatible with FluentValidation?
Section titled “Is Result compatible with FluentValidation?”Yes. Map FluentValidation results into Pragmatic errors:
var validation = await validator.ValidateAsync(request);if (!validation.IsValid) return new ValidationError(validation.Errors.First().ErrorMessage);For deeper integration, see Pragmatic.Validation which generates validators automatically.
Can I use Result in library code without ASP.NET Core?
Section titled “Can I use Result in library code without ASP.NET Core?”Yes. The core Pragmatic.Result package has no ASP.NET Core dependency. It targets netstandard2.0 and works in any .NET project. The ASP.NET Core integration (Pragmatic.Result.AspNetCore) is a separate package.
How do I test methods that return Result?
Section titled “How do I test methods that return Result?”// Assert successvar result = service.GetUser(42);result.IsSuccess.Should().BeTrue();result.Value.Id.Should().Be(42);
// Assert failurevar result = service.GetUser(999);result.IsFailure.Should().BeTrue();result.Error.Code.Should().Be("NOT_FOUND");
// For VoidResultvar result = service.ValidateInput(input);result.IsSuccess.Should().BeTrue();
// Mock a Result-returning methodmock.Setup(s => s.GetUserAsync(42)) .ReturnsAsync(Result<User, NotFoundError>.Success(testUser));
mock.Setup(s => s.GetUserAsync(999)) .ReturnsAsync(NotFoundError.Create("User", 999));Getting Help
Section titled “Getting Help”- GitHub Issues: github.com/nicola-pragmatic/Pragmatic.Design/issues
- Concepts Guide: See concepts.md for architecture and design decisions.
- Common Mistakes: See common-mistakes.md for patterns to avoid.
- API Reference: See api-reference.md for complete method documentation.
- Migration Guide: See migration.md for migrating from exceptions or other Result libraries.