What you'll learn: Practical guidelines for writing idiomatic Rust — code organization, naming conventions, error handling patterns, and documentation. A quick-reference chapter you'll return to often.
calculate_total_price() vs calc()/// for public APIsunwrap() unless infallible: Only use when you're 100% certain it won't panic// Bad: Can panic
let value = some_option.unwrap();
// Good: Handle the None case
let value = some_option.unwrap_or(default_value);
let value = some_option.unwrap_or_else(|| expensive_computation());
let value = some_option.unwrap_or_default(); // Uses Default trait
// For Result<T, E>
let value = some_result.unwrap_or(fallback_value);
let value = some_result.unwrap_or_else(|err| {
eprintln!("Error occurred: {err}");
default_value
});
expect() with descriptive messages: When unwrap is justified, explain whylet config = std::env::var("CONFIG_PATH")
.expect("CONFIG_PATH environment variable must be set");
Result<T, E> for fallible operations: Let callers decide how to handle errorsthiserror for custom error types: More ergonomic than manual implementationsuse thiserror::Error;
#[derive(Error, Debug)]
pub enum MyError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Parse error: {message}")]
Parse { message: String },
#[error("Value {value} is out of range")]
OutOfRange { value: i32 },
}
? operator: Propagate errors up the call stackthiserror over anyhow: Our team convention is to define explicit error
enums with #[derive(thiserror::Error)] so callers can match on specific variants.
anyhow::Error is convenient for quick prototyping but erases the error type, making
it harder for callers to handle specific failures. Use thiserror for library and
production code; reserve anyhow for throwaway scripts or top-level binaries where
you only need to print the error.unwrap() is acceptable:
assert_eq!(result.unwrap(), expected)let numbers = vec![1, 2, 3];
let first = numbers.get(0).unwrap(); // Safe: we just created the vec with elements
// Better: Use expect() with explanation
let first = numbers.get(0).expect("numbers vec is non-empty by construction");
&T instead of cloning when possibleRc<T> sparingly: Only when you need shared ownership{} to control when values are droppedRefCell<T> in public APIs: Keep interior mutability internalcargo bench and profiling tools&str over String: When you don't need ownershipBox<T> for large stack objects: Move them to heap if neededWhen creating custom types, consider implementing these fundamental traits to make your types feel native to Rust:
use std::fmt;
#[derive(Debug)] // Automatic implementation for debugging
struct Person {
name: String,
age: u32,
}
// Manual Display implementation for user-facing output
impl fmt::Display for Person {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} (age {})", self.name, self.age)
}
}
// Usage:
let person = Person { name: "Alice".to_string(), age: 30 };
println!("{:?}", person); // Debug: Person { name: "Alice", age: 30 }
println!("{}", person); // Display: Alice (age 30)
// Copy: Implicit duplication for small, simple types
#[derive(Debug, Clone, Copy)]
struct Point {
x: i32,
y: i32,
}
// Clone: Explicit duplication for complex types
#[derive(Debug, Clone)]
struct Person {
name: String, // String doesn't implement Copy
age: u32,
}
let p1 = Point { x: 1, y: 2 };
let p2 = p1; // Copy (implicit)
let person1 = Person { name: "Bob".to_string(), age: 25 };
let person2 = person1.clone(); // Clone (explicit)
#[derive(Debug, PartialEq, Eq)]
struct UserId(u64);
#[derive(Debug, PartialEq)]
struct Temperature {
celsius: f64, // f64 doesn't implement Eq (due to NaN)
}
let id1 = UserId(123);
let id2 = UserId(123);
assert_eq!(id1, id2); // Works because of PartialEq
let temp1 = Temperature { celsius: 20.0 };
let temp2 = Temperature { celsius: 20.0 };
assert_eq!(temp1, temp2); // Works with PartialEq
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
struct Priority(u8);
let high = Priority(1);
let low = Priority(10);
assert!(high < low); // Lower numbers = higher priority
// Use in collections
let mut priorities = vec![Priority(5), Priority(1), Priority(8)];
priorities.sort(); // Works because Priority implements Ord
#[derive(Debug, Default)]
struct Config {
debug: bool, // false (default)
max_connections: u32, // 0 (default)
timeout: Option<u64>, // None (default)
}
// Custom Default implementation
impl Default for Config {
fn default() -> Self {
Config {
debug: false,
max_connections: 100, // Custom default
timeout: Some(30), // Custom default
}
}
}
let config = Config::default();
let config = Config { debug: true, ..Default::default() }; // Partial override
struct UserId(u64);
struct UserName(String);
// Implement From, and Into comes for free
impl From<u64> for UserId {
fn from(id: u64) -> Self {
UserId(id)
}
}
impl From<String> for UserName {
fn from(name: String) -> Self {
UserName(name)
}
}
impl From<&str> for UserName {
fn from(name: &str) -> Self {
UserName(name.to_string())
}
}
// Usage:
let user_id: UserId = 123u64.into(); // Using Into
let user_id = UserId::from(123u64); // Using From
let username = UserName::from("alice"); // &str -> UserName
let username: UserName = "bob".into(); // Using Into
use std::convert::TryFrom;
struct PositiveNumber(u32);
#[derive(Debug)]
struct NegativeNumberError;
impl TryFrom<i32> for PositiveNumber {
type Error = NegativeNumberError;
fn try_from(value: i32) -> Result<Self, Self::Error> {
if value >= 0 {
Ok(PositiveNumber(value as u32))
} else {
Err(NegativeNumberError)
}
}
}
// Usage:
let positive = PositiveNumber::try_from(42)?; // Ok(PositiveNumber(42))
let error = PositiveNumber::try_from(-5); // Err(NegativeNumberError)
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct User {
id: u64,
name: String,
email: String,
}
// Automatic JSON serialization/deserialization
let user = User {
id: 1,
name: "Alice".to_string(),
email: "alice@example.com".to_string(),
};
let json = serde_json::to_string(&user)?;
let deserialized: User = serde_json::from_str(&json)?;
For any new type, consider this checklist:
#[derive(
Debug, // [OK] Always implement for debugging
Clone, // [OK] If the type should be duplicatable
PartialEq, // [OK] If the type should be comparable
Eq, // [OK] If comparison is reflexive/transitive
PartialOrd, // [OK] If the type has ordering
Ord, // [OK] If ordering is total
Hash, // [OK] If type will be used as HashMap key
Default, // [OK] If there's a sensible default value
)]
struct MyType {
// fields...
}
// Manual implementations to consider:
impl Display for MyType { /* user-facing representation */ }
impl From<OtherType> for MyType { /* convenient conversion */ }
impl TryFrom<FallibleType> for MyType { /* fallible conversion */ }
String, Vec, HashMap etc.f32/f64Rc<T> instead)| Trait | Benefit | When to Use |
|---|---|---|
Debug | println!("{:?}", value) | Always (except rare cases) |
Display | println!("{}", value) | User-facing types |
Clone | value.clone() | When explicit duplication makes sense |
Copy | Implicit duplication | Small, simple types |
PartialEq | == and != operators | Most types |
Eq | Reflexive equality | When equality is mathematically sound |
PartialOrd | <, >, <=, >= | Types with natural ordering |
Ord | sort(), BinaryHeap | When ordering is total |
Hash | HashMap keys | Types used as map keys |
Default | Default::default() | Types with obvious defaults |
From/Into | Convenient conversions | Common type conversions |
TryFrom/TryInto | Fallible conversions | Conversions that can fail |