← back

Rust ownership: a few edge cases that still bite me

May 8, 2026

I've been writing Rust full-time for about three years and I still get caught by the same handful of borrow-checker surprises. Writing them down here so future-me has something to grep.

1. The split-borrow trap on struct fields

You'd think the compiler can see that self.a and self.b are disjoint, and it can — but only when you write code that lets it. The moment you go through a method on self, the whole receiver is borrowed.

impl Engine {
    fn step(&mut self) {
        let item = self.queue.pop();         // borrows self.queue, fine
        // self.metrics.record(...);         // would also borrow self.metrics — also fine on its own
        self.process(item);                  // BUT: this borrows self mutably, conflict.
    }
}

The fix is usually to drop down to free functions that take only what they need, or to do the work inline. The pattern I reach for most often is splitting the struct into smaller ones whose fields are accessed independently.

2. Closures that capture by move when you didn't ask

Spawning a task captures by move. That part is well-known. What's less obvious is when an inner closure inside a method drags self along with it because you used a field through self.. The fix is the usual destructure-first dance:

let Engine { queue, metrics, .. } = self;
tokio::spawn(async move {
    while let Some(item) = queue.recv().await {
        metrics.record(item);
    }
});

3. &'a T vs. impl AsRef<T> in public APIs

I keep flip-flopping on this. AsRef is more flexible for callers but it pollutes the lifetime story when you start storing the reference. My current rule of thumb: AsRef at the leaf, explicit lifetimes at the boundary.

4. The infamous "cannot return reference to local variable"

Less infamous: you can usually fix it by returning the owned value and letting the caller decide what to borrow. The temptation to add another lifetime parameter is almost always wrong.

Closing

None of these are deep. They're just the ones I keep tripping over. If you've found a clean pattern for case (1) that doesn't involve splitting the struct, send it my way — I'm always open to better ideas.