Skip to content

Core Concepts

Preview Results Reviewed 2026-09-30 purview-dev/results Star on GitHub 1 aspnetcore c-sharp csharp discriminated-unions dotnet error-handling minimal-apis nuget problem-details result-type roslyn source-generator union-types zodsharp

Result<TValue, TError> is a readonly record struct with three states. Everything else in the suite — the generator, the HTTP adapter, the ZodSharp bridge — is built on the behaviour described here.

StateHow it is reachedIsInitializedIsSuccessIsFailure
Uninitializeddefault(Result<TValue, TError>)falsefalsefalse
SuccessResult<TValue, TError>.Success(value)truetruefalse
FailureResult<TValue, TError>.Failure(error)truefalsetrue

IsSuccess and IsFailure carry [MemberNotNullWhen], so reading Value after an IsSuccess check flows the nullability information the compiler needs.

An expected failure is a value; a misuse is a bug. The contract is deliberately loud:

  • Value throws InvalidOperationException ("The value of a non-successful result cannot be accessed.") unless the result is a success.
  • Error throws InvalidOperationException ("The error of a non-failed result cannot be accessed.") unless the result is a failure.
  • Match, Map, Bind, MapError and the extension operations throw InvalidOperationException ("The result is uninitialized.") for default.

An uninitialized result is never treated as a failure or as a success. If that is too strict for a boundary — inspecting a result you did not create — use the probing methods instead of the throwing ones.

TryGetValue and TryGetError in ResultExtensions are the deliberate exception to the throwing contract: they report whether a value or an error is available and never throw, even for default.

if (result.TryGetError(out var error))
logger.LogWarning("Failed: {Error}", error);
FormExample
Generic factoryResult<Tenant, TenantError>.Success(tenant)
Non-generic factoryResult.Success<Tenant, TenantError>(tenant)
Implicit from the valueResult<int, string> ok = 42;
Implicit from the errorResult<int, string> failed = "not a number";

Result.Success/Result.Failure exist so a call site does not have to name the value type twice. The implicit conversions from TValue and TError are public contract, and they are also what lets a method body return a plain value or a non-union error.

ToString() is stable and is what the examples print:

StateOutput
SuccessSuccess(value)
FailureFailure(error)
UninitializedUninitialized

IResultValue is the non-generic, read-only view used by infrastructure that cannot be generic over the value and error types — the ASP.NET Core endpoint filter, for example.

MemberBehaviour
IsInitializedWhether the result was created at all
IsSuccessWhether the result represents success
SuccessValueThe successful value, or null when the result is not a success
ErrorValueThe error, or null when the result is not a failure

Its accessors never throw: the accessor that does not describe the current state returns null. That is what makes it safe for a framework component to inspect a result without knowing the two type arguments.

Value-style operations that do not belong on the result type itself — probing, fallbacks, side effects, constraints and the asynchronous combinators — live in src/src/Results/Extensions/Purview/Results/ResultExtensions.cs. Add new extension methods there rather than on Result<TValue, TError>, so the type’s own surface stays the three states and the four core combinators.