08 Sep 2026
Throwing exceptions to handle expected failures is one of the most common anti-patterns in .NET. "What if the entity is not found?" Throw an InvalidOperationException. "What if the user does not have permission?" Throw an UnauthorizedAccessException. But exceptions are expensive to allocate, they break the normal control flow, and they make your error handling unpredictable.
The Result pattern fixes this. Every CQRS handler returns Result or Result<T> instead of throwing exceptions. This provides a consistent error-handling contract across all operations. Success or failure, it is all explicit.
// In a query handler:
return Result.Success(paginatedList);
// In a command handler:
return Result.Success();
Clean, explicit, no ambiguity. The caller knows exactly what happened.
// On failure, use the appropriate factory method:
return Result.NotFound(request.Id.ToString()); // entity does not exist (non-generic)
return Result.NotFound<Payment?>(request.PaymentId); // entity does not exist (generic)
return Result.Conflict(request.Id.ToString()); // entity state prevents the operation
return Result.ValidationError("message"); // business rule validation failed
Each failure type maps to a semantic meaning. NotFound means the entity does not exist. Conflict means the entity is in a state that prevents the operation. ValidationError means the input failed a business rule. The caller can map these to HTTP status codes with confidence.
var result = await commandBus.SendAsync(command, CancellationToken.None);
if (result.IsFailure) return Results.Problem(result.Error.Description);
return Results.NoContent();
The endpoint checks IsSuccess or IsFailure and maps to the appropriate HTTP response. No try/catch blocks. No exception filters for expected failures. Just a clean if/else.
if (result.IsFailure) return Results.Problem(result.Error.Description);
Result is just a struct with a value and an error. No stack trace allocation, no exception unwinding.return Result.NotFound(request.Id.ToString());
return Result.Conflict(request.Id.ToString());
Result return type does not hide failure behind a void or a thrown exception. It is right there in the method signature.public async Task<Result> HandleAsync(UpsertBlogPostCommand request, CancellationToken cancellationToken)
The pattern distinguishes between operations that return data and those that do not:
Result (commands with no return value) requires string in the error factory methods. Use request.Id.ToString().Result<T> (queries or commands with a return value) accepts object. Pass request.Id directly.// Non-generic: commands
return Result.NotFound(request.Id.ToString());
// Generic: queries
return Result.NotFound<Payment?>(request.PaymentId);
Brand Website V3 implements the Result pattern across every CQRS handler. No exceptions for expected failures, no try/catch spaghetti, just clean error handling everywhere. Explore the full implementation at kiss-code.com.
I continuously build, learn and experiment with innovative technology. Allow me to share what I learn, with you.