In this lesson, you will learn how to enforce domain invariants—guaranteeing that your data is always in a valid state—by restricting object instantiation to fallible constructors.
The "Invalid State" Problem
In Rust, it is easy to create a struct with public fields that allow impossible values. Imagine a Percentage struct meant to hold a value between 0 and 100. If you define it as a simple struct Percentage(u8), a user could instantiate Percentage(255), creating an object that breaks your program's logical invariants.
Enforcing Invariants via Fallible Constructors
Instead of allowing users to construct the struct directly, you keep the fields private and provide a "fallible constructor"—a function that returns a Result<Self, E>. By returning a Result, you force the caller to handle the possibility of invalid input before they can even touch the struct.
pub struct Percentage(u8);
#[derive(Debug)]
pub enum ValidationError {
OutOfRange,
}
impl Percentage {
// The "Fallible Constructor"
pub fn new(value: u8) -> Result<Self, ValidationError> {
if value <= 100 {
Ok(Percentage(value))
} else {
Err(ValidationError::OutOfRange)
}
}
pub fn value(&self) -> u8 {
self.0
}
}
Why This Matters
When you use this pattern, you move the burden of validation from every single function that uses the data to the single point where the data is created. If a function accepts a Percentage as an argument, it can trust that the internal u8 is valid. You no longer need to write if value > 100 checks inside your business logic because the type system itself guarantees the validity of the data.
By making the constructor fallible, you turn runtime "illegal state" bugs into handled errors at the perimeter of your system. Once the object is created, the rest of your program can proceed with total confidence in the data's integrity.
Key Takeaway: Use fallible constructors to make invalid states unrepresentable by forcing validation to occur at the moment of creation.