Frontend Labs
⋯

68. How not to write Rust: an anti-pattern catalog

Each entry shows a bad form, the problem, and a fix. These are the comments code reviewers leave most often on Rust from people coming from Go, C, or Java.

1. .clone() to silence the borrow checker

// fragment
// BAD: copies the whole Vec on every call, just to satisfy the compiler
fn total(items: Vec<Item>) -> u64 { items.iter().map(|i| i.price).sum() }
let t = total(cart.items.clone());
// GOOD: borrow
fn total(items: &[Item]) -> u64 { items.iter().map(|i| i.price).sum() }
let t = total(&cart.items);

Cloning is sometimes the right call (small values, or handing owned data to a thread). Do it on purpose, never just to make an error go away.

2. unwrap() on input you do not control

// fragment
// BAD: a malformed header crashes the request task (DoS, §51.2)
let len: usize = req.headers()["content-length"].to_str().unwrap().parse().unwrap();
// GOOD: errors are values
let len: usize = req.headers().get("content-length")
    .and_then(|v| v.to_str().ok()).and_then(|s| s.parse().ok())
    .ok_or(ApiError::BadRequest("content-length"))?;

3. Rc<RefCell<T>> (or Arc<Mutex<T>>) everywhere

Wrapping every object in shared mutable pointers rebuilds a garbage-collected object graph, with runtime borrow panics and lock contention. Usually the data has one natural owner. Pass &mut down the call stack, use indices into a Vec (Ch 27), or send messages to a task that owns the state (an actor, Ch 38).

4. Blocking inside async

// fragment
// BAD: stalls a Tokio worker thread, and every task scheduled on it
async fn handler() -> String { std::fs::read_to_string("big.json").unwrap() }
// GOOD
async fn handler() -> std::io::Result<String> { tokio::fs::read_to_string("big.json").await }
// For CPU work (hashing, compression, image processing): tokio::task::spawn_blocking (§31.1)

5. A std::sync::MutexGuard held across .await

// fragment
// BAD: the guard lives across the await (not Send; deadlock-prone)
let mut cache = state.cache.lock().unwrap();
let fresh = fetch(&key).await;
cache.insert(key, fresh);
// GOOD: lock, copy out, unlock, await, lock again
let cached = state.cache.lock().unwrap().get(&key).cloned();
let value = match cached { Some(v) => v, None => {
    let fresh = fetch(&key).await;
    state.cache.lock().unwrap().insert(key, fresh.clone());
    fresh
}};

6. String parameters everywhere

fn greet(name: String) forces every caller to allocate. Take &str (or impl AsRef<str>), and take String only when the function stores the value. Then the caller decides whether to clone or move.

7. Stringly-typed errors

Result<T, String> cannot be matched on, has no source chain, and is impossible to handle selectively. Use thiserror enums in libraries and anyhow with .context() in binaries (§10.2–10.3).

8. Indexing loops instead of iterators

// fragment
// BAD: bounds check per access, and an off-by-one waiting to happen
for i in 0..v.len() { if v[i] > max { max = v[i]; } }
// GOOD
let max = v.iter().copied().max();

9. unsafe for performance before measuring

get_unchecked to avoid a bounds check that LLVM had already removed. A hand-rolled transmute where from_le_bytes exists. Each unsafe block needs a profile that justifies it and a SAFETY comment that proves it (§33.2). Most of the time the safe version compiles to the same code.

10. Giant generic signatures with no dyn escape

Seven type parameters threaded through every function make compile errors unreadable and compile times long. When the dispatch is not in a hot loop, Box<dyn Trait> or an enum is simpler and just as fast in practice (§17.2).

11. Ignoring #[must_use] results

let _ = tx.send(msg); and let _ = file.flush(); discard errors on purpose. Make sure that is your intent. Writing the reason in a comment (// receiver gone: client disconnected, nothing to do) turns a silent bug into a documented decision.

12. Unbounded channels and collections fed by the network

mpsc::unbounded_channel(), a Vec that grows per message, or a HashMap cache without eviction can be filled by an attacker or a slow consumer until the OOM killer stops the process. Use bounded channels for backpressure, cap sizes, and evict (Ch 27).


Generated from real-life-rust-guide.md · 8 Oct 2026 16:36