Rust from Basics to Mastery — Part 7
Manage errors and projects with Result, modules, and Cargo
The articles written on this blog were created with the goal of teaching. Also, the information in the article may be incorrect, so please be careful.
Rust expresses recoverable failures as Result. As the project grows, module boundaries, cargo settings, and tests, along with error types, become part of the code contract.
Result and question mark operators
use std::fs;
use std::io;
fn load_config(path: &str) -> Result<String, io::Error> {
let text = fs::read_to_string(path)?;
Ok(text)
}
? takes out the inner value if it is a success value, and returns an error from the current function if it fails. It is not a grammar that ignores errors, but a short expression delivered to the caller.
Adding context to errors
fn parse_level(text: &str) -> Result<u32, String> {
text.trim()
.parse::<u32>()
.map_err(|error| format!("invalid level: {error}"))
}
The library can return errors for the caller to handle, turning them into logs or user messages at program boundaries. If you panic for a predictable input error, you lose the opportunity for recovery.
Setting the public scope with modules
mod combat {
pub struct Damage(pub u32);
pub fn calculate(attack: u32, defense: u32) -> Damage {
Damage(attack.saturating_sub(defense).max(1))
}
}
Basic items are only exposed to modules, and only contracts with pub can be used. Hiding the internal implementation reduces the scope for changes to spread to other modules.
Cargo and testing
#[cfg(test)]
mod tests {
use super::combat;
#[test]
fn damage_is_at_least_one() {
assert_eq!(combat::calculate(5, 100).0, 1);
}
}
Cargo manages dependencies, builds, tests, and document creation with one project rule. If you keep the error type and module disclosure scope small, the contract that the user needs to know is also smaller.