← Back to Skills

Rust Patterns

rust

Write idiomatic Rust code with proper ownership, error handling, and patterns.

Rust Patterns

Guidelines for writing safe, performant Rust code.

When to Activate

Ownership and Borrowing

Prefer borrowing over ownership transfer

// GOOD - borrows the string
fn print_length(s: &str) {
    println!("Length: {}", s.len());
}

// BAD - takes ownership unnecessarily
fn print_length(s: String) {
    println!("Length: {}", s.len());
}

Use &str for string parameters, String for owned data

// GOOD
fn greet(name: &str) -> String {
    format!("Hello, {}!", name)
}

// Works with both &str and String
greet("Alice");
greet(&my_string);

Clone only when necessary

// GOOD - pass reference when possible
fn process(data: &[u8]) { ... }

// Clone when you need owned copy
let copy = data.clone();

// BAD - unnecessary clone
fn process(data: Vec<u8>) { ... }  // Takes ownership
process(data.clone());  // Forced to clone

Error Handling

Use Result for recoverable errors

use std::fs::File;
use std::io::{self, Read};

fn read_file(path: &str) -> Result<String, io::Error> {
    let mut file = File::open(path)?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    Ok(contents)
}

Use thiserror for custom errors

use thiserror::Error;

#[derive(Error, Debug)]
pub enum AppError {
    #[error("User {0} not found")]
    NotFound(u64),
    
    #[error("Invalid input: {0}")]
    Validation(String),
    
    #[error("Database error")]
    Database(#[from] sqlx::Error),
    
    #[error("IO error")]
    Io(#[from] std::io::Error),
}

Use anyhow for applications, thiserror for libraries

// Application code - use anyhow
use anyhow::{Context, Result};

fn main() -> Result<()> {
    let config = load_config()
        .context("Failed to load configuration")?;
    run_app(config)?;
    Ok(())
}

// Library code - use thiserror
#[derive(thiserror::Error, Debug)]
pub enum LibError { ... }

Option Handling

Use combinators instead of match

// GOOD - combinators
let name = user.map(|u| u.name).unwrap_or("Anonymous".to_string());

let result = items
    .first()
    .filter(|item| item.active)
    .map(|item| item.value);

// OK for complex logic
match user {
    Some(u) if u.verified => process(u),
    Some(u) => verify_and_process(u),
    None => handle_anonymous(),
}

Use if let for single-arm matches

// GOOD
if let Some(user) = find_user(id) {
    process(user);
}

if let Err(e) = operation() {
    log::error!("Failed: {}", e);
}

// BAD
match find_user(id) {
    Some(user) => process(user),
    None => {},
}

Structs and Enums

Use builder pattern for complex construction

#[derive(Default)]
pub struct Request {
    url: String,
    method: Method,
    headers: HashMap<String, String>,
    timeout: Duration,
}

impl Request {
    pub fn new(url: impl Into<String>) -> Self {
        Self {
            url: url.into(),
            ..Default::default()
        }
    }
    
    pub fn method(mut self, method: Method) -> Self {
        self.method = method;
        self
    }
    
    pub fn header(mut self, key: &str, value: &str) -> Self {
        self.headers.insert(key.to_string(), value.to_string());
        self
    }
}

// Usage
let req = Request::new("https://api.example.com")
    .method(Method::POST)
    .header("Content-Type", "application/json");

Use newtype pattern for type safety

// GOOD - distinct types
struct UserId(u64);
struct OrderId(u64);

fn find_user(id: UserId) -> Option<User> { ... }
fn find_order(id: OrderId) -> Option<Order> { ... }

// BAD - can mix up IDs
fn find_user(id: u64) -> Option<User> { ... }
fn find_order(id: u64) -> Option<Order> { ... }

Iterators

Use iterator adapters

// GOOD
let active_names: Vec<_> = users
    .iter()
    .filter(|u| u.active)
    .map(|u| &u.name)
    .collect();

let total: u64 = orders.iter().map(|o| o.amount).sum();

// BAD
let mut active_names = Vec::new();
for user in &users {
    if user.active {
        active_names.push(&user.name);
    }
}

Use collect into specific types

// Collect into HashMap
let map: HashMap<_, _> = items
    .iter()
    .map(|i| (i.id, i.name.clone()))
    .collect();

// Collect Results
let results: Result<Vec<_>, _> = items
    .iter()
    .map(|i| parse(i))
    .collect();

Traits

Implement standard traits

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct User {
    pub id: u64,
    pub name: String,
}

// Implement Display for user-facing output
impl std::fmt::Display for User {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}#{}", self.name, self.id)
    }
}

Use impl Trait for return types

// GOOD - hides implementation detail
fn active_users(users: &[User]) -> impl Iterator<Item = &User> {
    users.iter().filter(|u| u.active)
}

// Also for accepting closures
fn map_all<F>(items: &[Item], f: F) -> Vec<String>
where
    F: Fn(&Item) -> String,
{
    items.iter().map(f).collect()
}

Async

Use async/await with tokio or async-std

use tokio;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let response = reqwest::get("https://api.example.com/data").await?;
    let data: ApiResponse = response.json().await?;
    println!("{:?}", data);
    Ok(())
}

Use join for concurrent operations

use tokio::join;

async fn fetch_all() -> Result<(Users, Orders), Error> {
    let (users, orders) = join!(
        fetch_users(),
        fetch_orders()
    );
    Ok((users?, orders?))
}