Common Anti-Patterns
Recognize Rust anti-patterns early: they compile but create panics, perf debt, or unmaintainable graphs.
Recipe
// Anti-pattern
let data = fetch().unwrap().clone().clone();
// Prefer
let data = fetch().context("fetch")?;
process(&data);When to reach for this: Code review, clippy cleanup, and post-incident refactors.
Working Example
| Anti-pattern | Problem | Fix |
|---|---|---|
unwrap() in library | panic for callers | ? + typed errors |
clone() to appease borrow checker | hidden cost | restructure ownership, Arc, or lifetimes |
Rc<RefCell<T>> graph | runtime borrow panics | arena, indices, or unique ownership |
String everywhere | allocations | &str, Cow, Arc<str> |
async fn blocking | stalled runtime | spawn_blocking |
dyn Error everywhere | erasure, no context | concrete error enums + thiserror |
What this demonstrates:
- Anti-patterns often start as quick fixes
- Clippy flags many (
unwrap_used,redundant_clone) - Libraries must not panic on bad input
- Measure clones in hot paths
Deep Dive
Stringly Typed APIs
Use newtypes and enums instead of String status codes. unwrap in tests and prototypes only with plan to remove.
Async Anti-Patterns
std::thread::sleep in async, block_on inside runtime, unbounded spawn without backpressure.
Gotchas
- Banning clone entirely - sometimes correct at boundary. Fix: clone once at spawn, not per iteration.
- expect without message - opaque prod panics. Fix:
expect("invariant: ...")or Result. - Over-Arc - shared mutable everything. Fix: message passing between tasks.
- Copy-paste error types - inconsistent mapping. Fix: workspace error crate.
- Ignoring clippy in tests - bad habits spread. Fix: allow narrowly in test modules only.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
? operator | Fallible ops | Truly impossible invariant (document) |
| Typestate | illegal states | trivial two-state bool |
| Channels | task isolation | synchronous hot loop |
FAQs
unwrap in main?
Acceptable for bootstrap fatal misconfig with message.
clone in async spawn?
Often required for 'static; clone once before spawn.
Rc in GUI?
Single-thread UI may use Rc; still prefer clear ownership.
todo! in prod?
Never on reachable paths; use Err or feature gate.
collect then iterate?
Extra alloc; chain iterators if possible.
Box service?
OK at boundary; inject generics in core domain.
mutex in lazy_static?
Legacy pattern; prefer OnceLock/LazyLock.
serde Value everywhere?
Lose type safety; parse into structs early.
raw threads?
Prefer tokio tasks in async apps; threads for CPU pools.
how enforce?
clippy deny in CI, review checklist, arch tests.
Related
- Avoiding Clones
- Panic vs Recoverable Errors
- Fighting the Borrow Checker
- Rust Patterns Best Practices
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.