Parse, Don't validate: An Effective Error Handling Strategy
Rust is known for it's incredibly powerful type system, helping you catch many errors and bugs at compile time.
However, most developers don't use the type system to it's full potential and instead rely on runtime checks to ensure their code is correct, and to be fair, this is understandable because many Rust developers come from languages that don't offer such a powerful type system.
In this blog post, we'll explore a different approach for you to handle errors in Rust, one that relies on parsing data instead of validating it. This approach is called "Parse, don't validate".
But before that, let's look at the traditional way of handling errors in Rust.
The Traditional Way: Validate
If you come from languages like JavaScript and Python, you might be used to writing code like this:
What is wrong with this code?
In the code above, we are validating both the email and password fields. This is a common pattern in many programming languages, but it has some downsides:
The create_user
function is taking two string slices as argument without the need for them to be validated.
If we were re-use the create_user
function in another part of our codebase, we would have to re-validate the email and password fields again, and if we forget to validate the fields before using the create_user
function, we might insert invalid data into our database without noticing.
You might be thinking that we could just move the email and password validations to the create_user
function like this:
While this is not a bad idea generally, but there is a better approach to do that which leverages the Rust type system to it's full potential.
Parse, don't validate
The email and password fields can have their own types, we can create a Email
and Password
structs to represent them:
We can then define an associated function parse()
for each of these structs to parse the input data and handle the validation logic inside of that method:
This way, we have an Email
struct type with a private field which is a String
, and a parse
method that returns a Result<Email, String>
. The parse
method will validate the email and return an Email
struct if the email is valid, otherwise it will return an error message.
It's crucial to make the String
field private and not public, the reason for this is that it prevents the Email
type to be constructed without using the parse()
method. This enforces you and the other developers to always use the parse()
method to create an instance of the Email
type.
To access the field of the Email
struct, we can define another method as_str()
to return a reference to the inner String
field:
We can do the same thing for the password as well:
We can then update the create_user()
function to instead of taking two string slices, it will take an Email
and Password
struct:
And the create_user_handler()
function will be updated to use the parse()
method to parse the email and password fields:
This way you have an incredibly type safe code that is easy to read and maintain. The Email
and Password
structs are now responsible for parsing and validating the input data, and the create_user
function is only responsible for creating the user.
Since the String
value inside the structs are private, you can't construct new instances of the structs that enforces you to only use the parse()
method which ensures that any Email
type is 100% validated via the parse()
method.
Conclusion
In this blog post, we explored a different approach for handling errors in Rust, one that relies on parsing data instead of validating it. This approach is called "Parse, don't validate".
Instead of just taking raw data and validating it, you can create a different type for each value that needs some sort of validation, and then define an associated function parse()
and a method as_str()
to parse and access the inner value of the struct.
Let's break down the steps one last time:
- Create a new type for each value that needs validation.
- Define an associated function
parse()
for each type to parse and validate the input data. - Define a method
as_str()
for each type to access the inner value of the struct. - Update the functions to use the new types instead of raw data.
Go back to your older code and see if you can apply this approach, find the validations that you have done in your code, it could be email, password, phone number, or validating a JSON object, and try to create a new type for each of them just the way we did for the Email
and Password
types.