Introduction
After working through Rust code in production environments—from order processing pipelines to real-time trading systems—we've learned that Rust's compiler is incredibly good at preventing entire categories of bugs. But it's also unforgiving when we work against its grain. Common Rust mistakes: a practical guide for developers addresses the habits we see most often, the ones that slow down delivery and create technical debt even when the code compiles and passes tests.
These aren't theoretical edge cases. They're patterns that emerge when teams move fast, when timelines compress, or when developers new to Rust try to force solutions that work in languages like Go or Node.js. Let's walk through the practical mistakes and how to avoid them.
Unwrap Everywhere
The first mistake we see is calling unwrap() liberally throughout codebases. It's tempting: Option and Result types feel awkward when you come from dynamic languages, and unwrap() makes the error go away. In a trading system we worked on, a junior engineer wrapped nearly every database call in unwrap(), converting possible errors into panics. The service ran fine in testing, then crashed in production when a connection pool was exhausted.
The issue isn't that unwrap exists—it has valid uses in test code and scripts. The problem is using it as a band-aid when the real work is handling errors properly. When we unwrap() in production code, we're saying "this will never fail," but we're rarely certain enough to stake the application's stability on it.
// Problematic: unwrap hides error handling
fn fetch_user(id: u64) -> User {
let user = db.query("SELECT * FROM users WHERE id = ?", id)
.unwrap(); // Will panic if query fails
user
}
// Better: propagate the error
fn fetch_user(id: u64) -> Result<User, DbError> {
let user = db.query("SELECT * FROM users WHERE id = ?", id)?;
Ok(user)
}
// Even better: provide context
fn fetch_user(id: u64) -> Result<User, anyhow::Error> {
db.query("SELECT * FROM users WHERE id = ?", id)
.with_context(|| format!("Failed to fetch user {}", id))
}
In production e-commerce systems, we use Result types throughout, then handle errors at API boundaries where we can return meaningful HTTP status codes and log the failure. The pattern is simpler than it first appears: use ? to propagate errors up, and catch them where you can actually do something about them.
Fighting the Borrow Checker with Rc<RefCell>
The borrow checker is Rust's superpower, but it's also what drives developers up the wall. When we hit a compilation error about borrowed values, the instinct is often to reach for Rc<RefCell<T>> and move on. In a CRM system we maintained, a developer wrapped nearly every domain entity in this pattern to avoid "dealing with lifetimes," and the code became a maze of runtime borrow failures.
Rc<RefCell<T>> trades compile-time guarantees for runtime panic potential. It moves the cost from "fix it now" to "debug it in production." In the CRM case, we eventually hit a deadlock scenario where code tried to mutably borrow a value that was already borrowed elsewhere—runtime panic, customer alert, incident.
Most of the time, when we feel forced to use Rc<RefCell<T>>, the real problem is our data structure design. Either we're trying to model circular references that don't actually exist, or we're holding references longer than necessary.
// Problematic: Rc<RefCell<>> as a crutch
struct UserService {
repository: Rc<RefCell<UserRepository>>,
}
impl UserService {
fn get_user(&self, id: u64) -> Result<User> {
let mut repo = self.repository.borrow_mut()?; // Can panic!
repo.find(id)
}
}
// Better: Own the repository, not reference it
struct UserService {
repository: UserRepository,
}
impl UserService {
fn get_user(&self, id: u64) -> Result<User> {
self.repository.find(id)
}
}
// If you need shared ownership without mutation, use Arc
use std::sync::Arc;
struct SharedReadOnlyData {
data: Arc<UserRepository>,
}
In practice, we structure services so each owns its dependencies directly. When we do need shared access across async tasks, we reach for Arc (atomic reference counting) instead of Rc, since we're often in async contexts. We rarely need the RefCell wrapper at all if we design the ownership model upfront.
Blocking in Async Code
Async Rust is powerful but has a critical gotcha: blocking operations in an async context starve other tasks. We've seen this tank the performance of ride-hailing and travel booking systems. A developer naively calls a synchronous database driver inside an async fn, and suddenly the system can't handle concurrent requests anymore.
The mistake is understandable. In languages like Node.js, there's a clear separation: if it's async, use await; if it's sync, just call it. Rust has both async and sync code living in the same runtime, and mixing them wrong causes thread pool starvation.
// Problematic: blocking sync call in async context
async fn process_order(order_id: u64) -> Result<()> {
let order = db.query(order_id).unwrap(); // Blocking! Starves other tasks
let user = fetch_user(order.user_id).await?;
Ok(())
}
// Better: use async-aware drivers
async fn process_order(order_id: u64) -> Result<()> {
let order = db.query_async(order_id).await?; // Non-blocking
let user = fetch_user(order.user_id).await?;
Ok(())
}
// If you must call sync code, offload to blocking thread pool
async fn process_order_with_legacy_db(order_id: u64) -> Result<()> {
let order = tokio::task::block_in_place(|| {
legacy_db.query(order_id)
})?;
let user = fetch_user(order.user_id).await?;
Ok(())
}
The solution is choosing async-first libraries (tokio, sqlx for database access, reqwest for HTTP) and avoiding sync libraries in async code. When legacy sync code is unavoidable, block_in_place explicitly moves it to a blocking thread pool so it doesn't starve the async runtime. It's slower, but it doesn't take down the whole system.
Premature Optimization with Unsafe
Unsafe Rust exists for performance-critical code that genuinely needs to bypass Rust's safety guarantees. But we've watched teams use it as a shortcut, writing unsafe blocks to save a few nanoseconds in code that's called rarely or in non-critical paths. One team added unsafe pointer arithmetic to a logging module—logging isn't performance-sensitive, and the unsafe code later caused a memory corruption bug that took weeks to track down.
The rule we follow: never write unsafe unless you've profiled and verified that safe Rust is actually the bottleneck. And when you do, document why, constrain it tightly, and test it heavily.
// Problematic: unsafe without measurement
fn process_items(items: &[Item]) -> u64 {
let mut sum = 0u64;
unsafe {
let ptr = items.as_ptr();
for i in 0..items.len() {
sum += (*ptr.add(i)).value; // Pointer arithmetic, no bounds check
}
}
sum
}
// Better: safe and likely just as fast
fn process_items(items: &[Item]) -> u64 {
items.iter().map(|item| item.value).sum()
}
// If unsafe is truly needed after profiling:
/// SAFETY: ptr must point to valid initialized Item,
/// and we assume the caller ensures the pointer is valid for reads.
unsafe fn process_item_ptr(ptr: *const Item) -> u64 {
(*ptr).value
}
Safe Rust is remarkably fast. Iterators get optimized away by LLVM. The performance difference between safe and unsafe code is often negligible. When it does matter—in a real-time trading engine, for instance—unsafe has its place, but only after profiling proves it's necessary.
Common Mistakes Summary
| Mistake | Why It Happens | Real Cost | Right Approach |
|---|---|---|---|
unwrap() everywhere |
Error handling feels verbose | Runtime panics in production | Use ? operator, handle at boundaries |
Rc<RefCell<T>> overuse |
Borrow checker frustration | Runtime borrow failures, deadlocks | Redesign ownership, use Arc for shared data |
| Blocking in async | Unfamiliar async semantics | Thread pool starvation, system slowdown | Use async-first libraries, block_in_place as last resort |
| Premature unsafe | Chasing micro-optimizations | Memory bugs, security vulnerabilities | Profile first, constrain unsafe narrowly, document invariants |
Key Takeaways
- Replace
unwrap()withResultpropagation using?and handle errors at API boundaries where you can act on them. - When the borrow checker feels like an obstacle, pause and reconsider your data structure—circular references and unnecessary shared mutability are usually the root cause.
- Build async systems with async-first libraries throughout. Blocking operations in async code don't just slow down one request; they degrade the entire system's throughput.
- Write unsafe code only after profiling proves safe Rust is the bottleneck. Most performance problems aren't where we think they are.
- These mistakes aren't failures—they're learning signposts. Each one teaches something about how Rust's model differs from what we're used to, and that knowledge compounds.






Comments
Loading comments…