Anti Patterns
Want to avoid common Rust mistakes? This guide identifies anti-patterns across all major Rust domains with explanations of why they’re problematic and how to fix them.
Ownership Mistakes
Excessive Cloning
Anti-pattern:
fn process_data(data: Vec<i32>) -> Vec<i32> {
data.clone() // Unnecessary clone
}
fn main() {
let data = vec![1, 2, 3];
let result = process_data(data.clone()); // Another clone!
// data is still valid, but we cloned twice
}Problem: Cloning is expensive. Each clone allocates and copies heap data.
Solution:
fn process_data(data: &[i32]) -> Vec<i32> {
data.to_vec() // One allocation
}
fn main() {
let data = vec![1, 2, 3];
let result = process_data(&data); // No clone, just borrow
// data still valid
}Why better: Borrow instead of clone. Clone only when you truly need owned copy.
Fighting the Borrow Checker
Anti-pattern:
fn bad_approach(data: &mut Vec<i32>) {
let first = &data[0]; // Immutable borrow
data.push(1); // ❌ Error: mutable borrow while immutable borrow exists
println!("{}", first);
}Attempted “fixes” that are wrong:
// Wrong: Using clone to work around borrow checker
let first = data[0].clone(); // Copy to avoid borrow
// Wrong: Using unsafe
unsafe {
let first = data.get_unchecked(0);
data.push(1);
}Problem: Working around borrow checker instead of understanding it.
Solution:
fn good_approach(data: &mut Vec<i32>) {
let first = data[0]; // Copy (i32 implements Copy)
data.push(1); // ✅ OK: first is copy, not borrow
println!("{}", first);
}
// Or:
fn good_approach2(data: &mut Vec<i32>) {
println!("{}", data[0]); // Use immediately, borrow ends
data.push(1); // ✅ OK: borrow ended
}Why better: Understand why borrow checker complains. Usually indicates real issue.
Misunderstanding Lifetimes
Anti-pattern:
struct Wrapper<'a> {
data: &'a str,
}
impl<'a> Wrapper<'a> {
fn get_data(&self) -> &'a str { // Wrong: returns 'a, not &self lifetime
self.data
}
}Problem: Over-constraining lifetime. 'a might outlive &self.
Solution:
impl<'a> Wrapper<'a> {
fn get_data(&self) -> &str { // Correct: elides to &self lifetime
self.data
}
}Why better: Lifetime elision handles this correctly. Don’t annotate unnecessarily.
Using Rc When Arc Is Needed
Anti-pattern:
use std::rc::Rc;
use std::thread;
fn main() {
let data = Rc::new(vec![1, 2, 3]);
let data_clone = Rc::clone(&data);
thread::spawn(move || { // ❌ Error: Rc doesn't implement Send
println!("{:?}", data_clone);
});
}Problem: Rc is not thread-safe. Can’t send between threads.
Solution:
use std::sync::Arc;
use std::thread;
fn main() {
let data = Arc::new(vec![1, 2, 3]);
let data_clone = Arc::clone(&data);
thread::spawn(move || { // ✅ OK: Arc implements Send
println!("{:?}", data_clone);
});
}Why better: Arc uses atomic operations, safe for threading.
Error Handling Anti-Patterns
Overusing unwrap() and expect()
Anti-pattern:
pub fn read_config(path: &str) -> Config {
let contents = std::fs::read_to_string(path).unwrap(); // Panics if file missing
serde_json::from_str(&contents).unwrap() // Panics if invalid JSON
}Problem: Crashes program on error. Denies callers chance to handle gracefully.
Solution:
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ConfigError {
#[error("IO error")]
Io(#[from] std::io::Error),
#[error("Parse error")]
Parse(#[from] serde_json::Error),
}
pub fn read_config(path: &str) -> Result<Config, ConfigError> {
let contents = std::fs::read_to_string(path)?;
let config = serde_json::from_str(&contents)?;
Ok(config)
}Why better: Callers can handle errors appropriately.
When unwrap() is okay: Tests, examples, prototypes.
Swallowing Errors
Anti-pattern:
fn process_files(paths: Vec<&str>) {
for path in paths {
let _ = std::fs::read_to_string(path); // Ignores errors silently
// Processing continues even if read failed
}
}Problem: Errors ignored. Silent failures are hard to debug.
Solution:
fn process_files(paths: Vec<&str>) -> Result<(), std::io::Error> {
for path in paths {
let contents = std::fs::read_to_string(path)?; // Propagate errors
// Process contents
}
Ok(())
}
// Or collect results:
fn process_files_all(paths: Vec<&str>) -> Vec<Result<String, std::io::Error>> {
paths.into_iter()
.map(|path| std::fs::read_to_string(path))
.collect()
}Why better: Errors visible and handleable.
Poor Error Messages
Anti-pattern:
fn parse_port(s: &str) -> Result<u16, String> {
s.parse().map_err(|_| "error".to_string()) // What error?
}Problem: Error message provides no context.
Solution:
fn parse_port(s: &str) -> Result<u16, String> {
s.parse()
.map_err(|e| format!("Failed to parse port '{}': {}", s, e))
}
// Better: use anyhow for context
use anyhow::Context;
fn parse_port(s: &str) -> anyhow::Result<u16> {
s.parse()
.with_context(|| format!("Failed to parse port from '{}'", s))
}Why better: Helpful error messages save debugging time.
Panic in Library Code
Anti-pattern:
pub fn get_user(id: u32, users: &[User]) -> User {
users.iter()
.find(|u| u.id == id)
.expect("User not found") // Panic in library!
}Problem: Libraries shouldn’t decide to crash the program.
Solution:
pub fn get_user(id: u32, users: &[User]) -> Option<&User> {
users.iter().find(|u| u.id == id)
}
// Or:
pub fn get_user(id: u32, users: &[User]) -> Result<&User, UserError> {
users.iter()
.find(|u| u.id == id)
.ok_or(UserError::NotFound(id))
}Why better: Caller decides how to handle absence.
Concurrency Pitfalls
Deadlocks from Improper Locking
Anti-pattern:
use std::sync::Mutex;
fn transfer(from: &Mutex<i32>, to: &Mutex<i32>, amount: i32) {
let mut from_account = from.lock().unwrap();
let mut to_account = to.lock().unwrap(); // Deadlock risk!
*from_account -= amount;
*to_account += amount;
}
// Thread A: transfer(account1, account2, 10)
// Thread B: transfer(account2, account1, 20) // Deadlock!
Problem: Different lock acquisition orders cause deadlock.
Solution:
use std::sync::Mutex;
fn transfer(from: &Mutex<i32>, to: &Mutex<i32>, amount: i32) {
// Acquire locks in consistent order
let (first, second) = if from as *const _ < to as *const _ {
(from, to)
} else {
(to, from)
};
let mut first_lock = first.lock().unwrap();
let mut second_lock = second.lock().unwrap();
// Perform transfer
}Why better: Consistent lock order prevents deadlocks.
Race Conditions with Unsafe
Anti-pattern:
static mut COUNTER: i32 = 0;
fn increment() {
unsafe {
COUNTER += 1; // Race condition!
}
}
// Multiple threads calling increment() have data race
Problem: Mutable static without synchronization causes undefined behavior.
Solution:
use std::sync::atomic::{AtomicI32, Ordering};
static COUNTER: AtomicI32 = AtomicI32::new(0);
fn increment() {
COUNTER.fetch_add(1, Ordering::SeqCst); // Thread-safe
}Why better: Atomic operations prevent data races.
Blocking in Async Code
Anti-pattern:
async fn bad_async_function() {
std::thread::sleep(Duration::from_secs(1)); // Blocks executor!
}Problem: Blocks executor thread, preventing other tasks from running.
Solution:
async fn good_async_function() {
tokio::time::sleep(Duration::from_secs(1)).await; // Yields to executor
}
// For CPU-intensive work:
async fn cpu_intensive() {
tokio::task::spawn_blocking(|| {
// Heavy computation
}).await.unwrap();
}Why better: Async runtime can schedule other tasks while waiting.
Memory Leaks with Arc Cycles
Anti-pattern:
use std::rc::Rc;
use std::cell::RefCell;
struct Node {
next: Option<Rc<RefCell<Node>>>,
prev: Option<Rc<RefCell<Node>>>, // Creates cycle!
}
// Nodes hold strong references to each other, never drop
Problem: Reference cycle prevents deallocation.
Solution:
use std::rc::{Rc, Weak};
use std::cell::RefCell;
struct Node {
next: Option<Rc<RefCell<Node>>>,
prev: Option<Weak<RefCell<Node>>>, // Weak reference breaks cycle
}Why better: Weak references don’t prevent deallocation.
Type System Misuse
Primitive Obsession
Anti-pattern:
fn charge_customer(
customer_id: i64,
order_id: i64,
amount: f64,
currency: String,
) {
// Easy to swap customer_id and order_id
// Easy to use wrong currency
}Problem: Primitive types provide no semantic meaning.
Solution:
struct CustomerId(i64);
struct OrderId(i64);
struct Money {
amount: f64,
currency: Currency,
}
enum Currency {
USD,
EUR,
GBP,
}
fn charge_customer(
customer_id: CustomerId,
order_id: OrderId,
money: Money,
) {
// Type system prevents mistakes
}Why better: Types document intent and prevent errors.
Stringly-Typed APIs
Anti-pattern:
fn execute_command(command: &str, args: Vec<String>) -> Result<String, String> {
match command {
"create" => { /* ... */ },
"delete" => { /* ... */ },
_ => Err("Unknown command".to_string()), // Runtime error
}
}Problem: Errors caught at runtime, not compile time.
Solution:
enum Command {
Create { name: String },
Delete { id: u32 },
}
fn execute_command(command: Command) -> Result<Output, Error> {
match command {
Command::Create { name } => { /* ... */ },
Command::Delete { id } => { /* ... */ },
} // Exhaustiveness checked at compile time
}Why better: Compiler ensures all commands handled.
Over-Reliance on Any
Anti-pattern:
use std::any::Any;
fn process(value: &dyn Any) {
if let Some(s) = value.downcast_ref::<String>() {
// Process String
} else if let Some(i) = value.downcast_ref::<i32>() {
// Process i32
}
// Lost type safety
}Problem: Dynamic typing defeats Rust’s type system.
Solution:
enum Value {
Text(String),
Number(i32),
}
fn process(value: Value) {
match value {
Value::Text(s) => { /* Process String */ },
Value::Number(i) => { /* Process i32 */ },
}
}Why better: Type-safe, exhaustiveness checked.
Fighting the Type System
Anti-pattern:
use std::mem;
fn bad_type_coercion<T, U>(value: T) -> U {
unsafe { mem::transmute_copy(&value) } // Very dangerous!
}Problem: Circumventing type system causes undefined behavior.
Solution:
// Use proper conversions
impl From<Celsius> for Fahrenheit {
fn from(c: Celsius) -> Fahrenheit {
Fahrenheit(c.0 * 9.0 / 5.0 + 32.0)
}
}
let f: Fahrenheit = celsius.into();Why better: Type-safe conversions prevent undefined behavior.
Performance Anti-Patterns
Premature Optimization
Anti-pattern:
// Optimizing before measuring
fn process(data: &[i32]) -> Vec<i32> {
let mut result = Vec::with_capacity(data.len()); // Maybe not needed
unsafe {
// Unsafe code for "performance"
}
}Problem: Optimizations add complexity without proven benefit.
Solution:
// Start simple
fn process(data: &[i32]) -> Vec<i32> {
data.iter().map(|x| x * 2).collect()
}
// Profile to find bottlenecks
// Optimize only if proven slow
Why better: Simple code is easier to maintain. Optimize when measurements justify it.
Unnecessary Boxing
Anti-pattern:
fn create_string() -> Box<String> {
Box::new(String::from("hello")) // Why Box?
}
let s = create_string();
println!("{}", *s); // Must dereference
Problem: Extra heap allocation and indirection for no reason.
Solution:
fn create_string() -> String {
String::from("hello")
}
let s = create_string();
println!("{}", s); // Direct access
Why better: Fewer allocations, simpler code.
Collecting Unnecessarily
Anti-pattern:
let sum: i32 = numbers.iter()
.filter(|x| **x > 0)
.collect::<Vec<_>>() // Unnecessary allocation
.iter()
.sum();Problem: Intermediate collection allocates memory unnecessarily.
Solution:
let sum: i32 = numbers.iter()
.filter(|x| **x > 0)
.sum(); // No intermediate collection
Why better: Iterator chains are lazy and allocation-free until consumed.
Ignoring Compiler Warnings
Anti-pattern:
fn unused_function() { // Warning: never used
// ...
}
let x = 5; // Warning: unused variable
Problem: Warnings indicate potential issues. Ignoring them allows bugs.
Solution:
// Remove unused code
// Or if intentionally unused:
#[allow(dead_code)]
fn maybe_used_later() {
// ...
}
let _x = 5; // Prefix _ to indicate intentionally unused
Why better: Clean warning-free codebase catches real issues.
Code Organization Issues
God Modules
Anti-pattern:
// src/lib.rs or src/main.rs with thousands of lines
// Everything in one file/module
Problem: Hard to navigate, test, and maintain.
Solution:
// src/lib.rs
pub mod database;
pub mod models;
pub mod handlers;
pub mod utils;
// Each module has focused responsibility
// Each in separate file
Why better: Modular code is easier to understand and maintain.
Circular Dependencies
Anti-pattern:
// module_a.rs
use crate::module_b::TypeB;
pub struct TypeA {
b: TypeB,
}
// module_b.rs
use crate::module_a::TypeA;
pub struct TypeB {
a: TypeA, // Circular dependency!
}Problem: Circular dependencies indicate poor design.
Solution:
// Extract shared types to new module
// module_types.rs
pub struct TypeA { /* ... */ }
pub struct TypeB { /* ... */ }
// module_a.rs
use crate::module_types::{TypeA, TypeB};
// module_b.rs
use crate::module_types::{TypeA, TypeB};Why better: Clear dependency hierarchy.
Poor Abstraction Boundaries
Anti-pattern:
pub struct Database {
pub connection_string: String, // Implementation detail exposed
pub connection_pool: Vec<Connection>, // Should be private
}Problem: Implementation details leak into public API.
Solution:
pub struct Database {
connection_string: String, // Private
connection_pool: Vec<Connection>, // Private
}
impl Database {
pub fn new(connection_string: String) -> Self {
// Construct with implementation details hidden
}
pub fn query(&self, sql: &str) -> Result<QueryResult> {
// Public interface without exposing internals
}
}Why better: Can change implementation without breaking API.
Learning from Mistakes
How to Identify Anti-Patterns
- Compiler warnings: Heed them, don’t suppress
- Clippy: Run regularly, fix suggestions
- Code review: Have others review your code
- Community: Ask in forums if pattern seems awkward
- Documentation: Read idiomatic code (std library, popular crates)
When in Doubt
- KISS: Keep it simple
- Measure: Profile before optimizing
- Read: Study idiomatic Rust code
- Ask: Community is helpful and welcoming
Related Content
- Best Practices - Idiomatic patterns
- Cookbook - Correct recipes
- Tutorials - Learn fundamentals properly
Avoid these anti-patterns to write better, safer Rust code!