Last active
June 3, 2023 17:55
-
-
Save squarism/e914948ea96dccbff1420d903a270729 to your computer and use it in GitHub Desktop.
Railroad Error Handling in Rust
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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