Using exceptions for control flow in .NET applications leads to poor performance, unclear code intent, and makes it difficult to distinguish between expected failures and truly exceptional circumstances. The Result pattern provides a clean, explicit way to handle errors without the overhead and complexity of exceptions.
Traditional error handling relies heavily on exceptions, even for expected failure scenarios like validation errors or "not found" cases. This approach has several drawbacks:
public class UserService{public User GetUserById(int id){var user = _repository.FindById(id);if (user == null)throw new NotFoundException($"User {id} not found");if (!user.IsActive)throw new InvalidOperationException("User is not active");return user;}}// Calling codetry{var user = userService.GetUserById(123);// Process user}catch (NotFoundException ex){// Handle not found}catch (InvalidOperationException ex){// Handle invalid operation}catch (Exception ex){// Handle unexpected errors}
❌ Figure: Bad example - Using exceptions for expected failures leads to performance issues and unclear code
The Result pattern represents the outcome of an operation as a return value, explicitly indicating success or failure with relevant error information. This makes error handling first-class, improves performance, and clarifies code intent.
public class UserService{public ErrorOr<User> GetUserById(int id){var user = _repository.FindById(id);if (user == null)return Error.NotFound(description: $"User {id} not found");if (!user.IsActive)return Error.Validation(description: "User is not active");return user;}}// Calling codevar result = userService.GetUserById(123);return result.Match(user => Ok(user),errors => Problem(errors));
✅ Figure: Good example - Using the Result pattern makes error handling explicit and performant
Benefits of the Result pattern:
Several excellent libraries implement the Result pattern in .NET. Here's a comparison of the most popular options:
| Library | GitHub Stars | Key Features | Pros | Cons | Best For |
| ErrorOr | ~1.8k | Discriminated union, fluent API, pattern matching | Simple and intuitive, excellent pattern matching, minimal dependencies, great for Clean Architecture & VSA. Multiple built-in results such as NotFound, Conflict, etc. | Smaller ecosystem compared to FluentResults | ✅ Recommended - Modern .NET apps, Clean Architecture & VSA, DDD |
| FluentResults | ~2.3k | Fluent API, logging support, rich metadata | Mature library, extensive features, good logging integration | More complex API, heavier dependency. Need to roll your own result types such as NotFound, Conflict, etc | Complex enterprise apps needing detailed error tracking |
| Ardalis.Result | ~950+ | Status codes, ASP.NET Core integration, simple API | Great ASP.NET Core integration, clear status mappings, easy to learn | Less flexible error types, ASP.NET Core focused | ASP.NET Core MVC/API projects with straightforward needs |
ErrorOr is the recommended choice for most .NET projects
ErrorOr, created by Amichai Mantinband, offers the best balance of simplicity, functionality, and modern C# patterns:
Installation
dotnet add package ErrorOr
Basic Usage
using ErrorOr;public class ProductService{public ErrorOr<Product> CreateProduct(string name, decimal price){// Validationif (string.IsNullOrWhiteSpace(name))return Error.Validation(description: "Product name is required");if (price <= 0)return Error.Validation(description: "Price must be greater than zero");// Business logicvar product = new Product { Name = name, Price = price };_repository.Add(product);return product;}}// In your controller/endpoint[HttpPost]public IActionResult CreateProduct(CreateProductRequest request){var result = _productService.CreateProduct(request.Name, request.Price);return result.Match(product => Ok(product),errors => Problem(errors));}
✅ Figure: ErrorOr makes it easy to handle success and failure cases with pattern matching
Use the Result pattern for:
Use exceptions for:
Learn more: