Sponsored
Many thanks to the sponsors who make it possible for this newsletter to be free for readers. Become a sponsor.
Exceptions should be rare. Why? Throwing and catching exceptions is slow relative to other code flow patterns. Because of this, exceptions shouldn't be used to control normal program flow. Also... Code that relies heavily on exceptions for control flow can become difficult to read and maintain . It can be challenging to follow the logic of a program that jumps from one exception handler to another, as opposed to one that follows a more straightforward, linear flow. Also... Improperly handled exceptions can lead to resource leaks. Exceptions are designed to handle unexpected and rare events . Using them for regular control flow, like handling business logic or validations, is generally considered a bad practice because it misrepresents the intention of the exception mechanism. Even Microsoft has a recommendation:
public class UserRegistration{ public void RegisterUser(string username, string password) { if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password)) { throw new ArgumentException("Username and password are required."); } if (password.Length < 8) { throw new ArgumentException("Password must be at least 8 characters long."); } // Proceed with registration }}
In this approach, validation failures are treated as exceptions, which is not ideal for common scenarios like invalid input. They are best reserved for truly exceptional, unforeseen, and irregular situations. For regular and predictable events like input validation, standard control flow mechanisms (like Result<T>) are more appropriate and efficient.
By using Result, you're ensuring that your code is handling expected scenarios (like invalid user input) in a more predictable and maintainable way, improving the overall quality and readability of your codebase. Let's see the same example with Result<T> object:
public class UserRegistration{ public Result RegisterUser(string username, string password) { if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password)) { return Result.Failure("Username and password are required."); } if (password.Length < 8) { return Result.Failure("Password must be at least 8 characters long."); } // Proceed with registration return Result.Success(); }}
Here the flow is quite normal, we return values in relation to the code being executed instead of throwing an exception where it is not necessary. Result<T> does not come with any library (there are various libraries that already implement the Result object, like FluentResults) which means that I created it myself. Let me show you how:
public class Result{ public bool IsSuccess { get; private set; } public bool IsFailure { get; private set; } => !IsSuccess public string ErrorMessage { get; private set; } protected Result(bool isSuccess, string errorMessage) { IsSuccess = isSuccess; ErrorMessage = errorMessage; } public static Result Success() => new Result(true, null); public static Result Failure(string message) => new Result(false, message);}
csharp
public record Error(string Type, string Description)
{
public static readonly Error None = new(string.Empty, string.Empty);
}
And now for each of the failed validations we can create a separate Error object that will represent a unique error for that type of validation:
public static class RegistrationErrors{ public static readonly Error UsernameAndPasswordRequired = new Error( "Registration.UsernameAndPasswordRequired", "Username and password are required."); public static readonly Error PasswordTooShort = new Error( "Registration.PasswordTooShort", "Password must be at least 8 characters long.");}
So, instead of an ordinary string, we can return a more structured value (this means that Error in the Result<T> object should be an Error type, and no longer a string):
return Result.Failure(RegistrationErrors.PasswordTooShort);
Improved Readability : The Result pattern clearly indicates that user input validation is a part of the normal flow and not an exceptional circumstance. Easier Error Handling: It guides the caller to handle both success and failure cases explicitly, making the code more robust. Enhanced Performance: Avoiding exceptions for regular control flow scenarios like input validation is more performance-efficient. Flexibility and Extensibility: The Result pattern can easily be extended or modified to include additional details about the failure or even success scenarios, without changing the method signature.
For API-specific error handling, see ProblemDetails in ASP.NET Core.
Let's distill the essence of this week's discussion: reserve exceptions for truly unforeseen events. They are best suited for situations where the error is beyond your immediate handling capabilities. For everything else, the clarity and structure offered by the Result pattern are far more beneficial. Embracing the Result class in your code allows you to:
Want to enforce clean code automatically? My Pragmatic .NET Code Rules course shows you how to set up analyzers, CI quality gates, and architecture tests - a production-ready system that keeps your codebase clean without manual reviews. Or grab the free Starter Kit to try it out.
Stop arguing about code style. In this course you get a production-proven setup with analyzers, CI quality gates, and architecture tests — the exact system I use in real projects. Join here.
Not sure yet? Grab the free Starter Kit — a drop-in setup with the essentials from Module 01.
Design Patterns that Deliver — Solve real problems with 5 battle-tested patterns (Builder, Decorator, Strategy, Adapter, Mediator) using practical, real-world examples. Trusted by 650+ developers.
Just getting started? Design Patterns Simplified covers 10 essential patterns in a beginner-friendly, 30-page guide for just $9.95.
Every Monday morning, I share 1 actionable tip on C#, .NET & Architecture that you can use right away. Join here.
Join 20,000+ subscribers who mass-improve their .NET skills with actionable tips on C#, Software Architecture & Best Practices.
Subscribe to the TheCodeMan.net and be among the 20,000+ subscribers gaining practical tips and resources to enhance your .NET expertise.