Skip to content

Instantly share code, notes, and snippets.

@squarism
Last active June 3, 2023 17:55
Show Gist options
  • Save squarism/e914948ea96dccbff1420d903a270729 to your computer and use it in GitHub Desktop.
Save squarism/e914948ea96dccbff1420d903a270729 to your computer and use it in GitHub Desktop.
Railroad Error Handling in Rust
// User password resets could benefit from railroad error handling
// because there are so many things to go wrong along the way
//
// 1. I forgot my password, send me a link
// 2. I got the link, let me enter a new password
// 3. Password and confirmation must match
// 4. Password must be at least 8, etc
// 5. Password was saved successfully
//
// There are many things that can go wrong and having a transaction
// to handle all of this would really be convenient. For example,
// we want to keep the reset link active if the password saving doesn't
// work or if it doesn't match or meet password requirements. Wrapping all
// this in a transaction would be much easier than having to clean up
// things after everything is over. But to keep things simple, we will
// have to ignore database transactions.
//
// The real demonstration is on how to handle errors along the way.
// this is a user, say, from a database
// the view would protect the fields like password from being shown
#[derive(PartialEq, Clone)]
struct User<'a> {
email: &'a str,
password: &'a str,
reset_link: Option<&'a str>,
}
fn forgot_password_link<'a>(users: &mut [User<'a>], email: &str) -> Result<User<'a>, &'a str> {
if let Some(user_to_email) = users.iter_mut().find(|u| u.email == email) {
// send email here
// return user as success
Ok(user_to_email.clone())
} else {
Err("No user found")
}
}
fn reset_password<'a>(
mut user: User<'a>,
new_password: &'a str,
new_password_confirm: &str,
) -> Result<User<'a>, &'a str> {
if new_password != new_password_confirm {
return Err("Passwords must match.");
}
if new_password.len() < 8 {
return Err("Password needs to be at least 8.");
}
user.password = new_password;
Ok(user)
}
fn save_password<'a>(mut database: Vec<User<'a>>, user: &User<'a>) -> Result<User<'a>, &'a str> {
if let Some(user_to_update) = database.iter_mut().find(|u| u.email == user.email) {
*user_to_update = user.clone();
Ok(user.clone())
} else {
Err("No user found")
}
}
fn delete_reset_link<'a>(user: &'a User<'a>) -> Result<User<'a>, &'a str> {
// not shown: deleting the link field or row from a database
Ok(user.clone())
}
fn main() {
let user: User = User {
email: "[email protected]",
password: "12345678",
reset_link: None,
};
// this represents state, no time to build a database
let mut users = vec![user.clone()];
// No matter what, the system tells you a password reset link was sent for security.
// This detail is hard to represent but we would handle a Err very similarly to
// Ok, but in this case, this demo is illustrating what is happening by printing.
let result = forgot_password_link(&mut users, "[email protected]");
match result {
Ok(user) => println!("Check your email: {:?}", user.email),
Err(s) => println!("I will still tell you to check your email: {}", s),
}
// Along the way, we get results. The return could be the each step's data
// that it just worked on. If we just return an ID or some metadata like status
// then we could go to the database to fetch the data we are going to work on next.
// In this example, I am returning `user` because the User is the only thing being worked
// on for password resets.
//
// This whole block would be put inside of a transaction (except for the emailing)
// because it needs to be all or nothing.
let user = reset_password(user, "new-password", "new-password").expect("Cannot reset password");
println!("Password was reset to: {:?}", user.password);
// In order to see errors as they happen, you can introduce problems since this
// program is not interactive. This is an example of how to introduce an error.
// user.email = "invalid";
let user = save_password(users, &user).expect("Could not save password");
println!("Saved password for: {:?}", user.email);
let user = delete_reset_link(&user).expect("Could not remove password link");
println!("Deleted the password link for user: {:?}", user.email);
}
// How to introduce errors:
// 1. forgot_password_link(): change "[email protected]" to "[email protected]"
// 2. reset_password(): change one of the "new-password" to "asdf" or mismatched strings
// 3. save_password(): before save_password is called, mutate the user like `user.email = "invalid"`
// 4. delete_reset_link(): there is no way to fail this step in this demo
// These things (steps 2, 3 and 4) would be collected into a transaction.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment