LEARN · DEBUGGING GUIDE

Cannot Borrow Error: Practical Fixes for Real Code

Stop fighting the borrow checker. This guide breaks down the three real patterns behind 'cannot borrow' errors and shows you exactly how to fix each one.

IntermediateRust7 min read

What this usually means

At its core, 'cannot borrow' means the Rust compiler has detected that you are trying to access memory in a way that could violate aliasing guarantees. Rust's ownership model enforces that at any given time, you have either one mutable reference or any number of immutable references. When you see 'cannot borrow', the compiler has found a code path where you hold a reference (immutable or mutable) and then try to create another reference that would conflict. This isn't a bug in the compiler—it's the compiler preventing a data race or use-after-free. The fix is to restructure your code to respect these rules, not to fight them.

( 01 )Fast diagnosis

The first ten minutes — establish facts before touching code.

  • 11. Read the full error message: the compiler tells you exactly which borrow is conflicting and where.
  • 22. Identify the conflicting borrows: look for the two references mentioned in the error.
  • 33. Determine if you need both references simultaneously: if not, drop one before creating the other.
  • 44. If you need both, consider using a cell type (RefCell, Mutex) or cloning the data.
  • 55. Check for non-lexical lifetimes (NLL): ensure your Rust edition is 2018+ and the error still persists.
  • 66. Run 'cargo clippy' — it often suggests the idiomatic fix for borrow checker issues.
( 02 )Where to look

The specific files, logs, configs, and dashboards that usually own this bug.

  • searchThe exact line and column numbers in the compiler error output
  • searchFunctions that take both &self and &mut self (or &self and &self methods) on the same object
  • searchLoops where you hold a reference to a collection while mutating it (e.g., `for item in vec.iter()` then `vec.push()` inside)
  • searchMatch arms that borrow different parts of a struct, especially with mutable fields
  • searchConditional branches where one branch holds a reference and another modifies the data
  • searchCallback closures that capture references to the surrounding environment
( 03 )Common root causes

Practical causes, not theory. These are the things you will actually find.

  • warningTrying to return a reference to a local variable (dangling reference)
  • warningHolding an immutable reference while calling a &mut self method on the same value
  • warningIterating over a collection and modifying it in the loop body
  • warningTwo closures or threads trying to capture the same data mutably
  • warningAttempting to move out of a reference (e.g., `*ref_to_option.take()` without proper ownership)
  • warningUsing `self` in two different methods called in the same scope (e.g., `obj.method1(); obj.method2();` where method1 borrows immutably and method2 mutably)
( 04 )Fix patterns

Concrete fix directions. Pick the one that matches your root cause.

  • buildRestructure to reduce scope of borrows: add braces to drop references before creating new ones.
  • buildUse `clone()` or `to_owned()` when you need independent data (but beware of performance).
  • buildSwitch to interior mutability with `RefCell` or `Mutex` for shared mutable state.
  • buildReplace `&mut self` methods with immutable methods that return a wrapper with mutable methods.
  • buildUse iterator adapters like `.enumerate()` or index-based loops to avoid borrowing the collection.
  • buildLeverage split borrows: borrow different fields of a struct separately (e.g., `let a = &mut x.a; let b = &mut x.b;`).
( 05 )How to verify

A fix you cannot prove is a guess. Close the loop.

  • verifiedAfter the fix, run `cargo build` and confirm no borrow checker errors.
  • verifiedRun `cargo test` to ensure existing logic still works.
  • verifiedRun `cargo clippy` to check for more idiomatic alternatives.
  • verifiedIf you used interior mutability, verify there are no runtime borrow panics by testing edge cases.
  • verifiedFor unsafe code, run with Miri (`cargo miri test`) to detect undefined behavior.
( 06 )Mistakes to avoid

Things that make this bug worse or harder to find.

  • warningUsing `unsafe` to circumvent the borrow checker—this usually leads to undefined behavior.
  • warningAdding `.clone()` everywhere without understanding the ownership trade-offs (performance loss).
  • warningIgnoring the compiler's suggestion (e.g., adding `&` or `mut` in the wrong place).
  • warningRefactoring blindly: change one borrow at a time and recompile.
  • warningAssuming that non-lexical lifetimes (NLL) fix all issues—they help but don't eliminate the need for correct structure.
( 07 )War story

The Case of the Mutating Iterator

Senior Backend EngineerRust 1.65, Actix-web 4, PostgreSQL via sqlx

Timeline

  1. 09:15Deploy new endpoint to update user profiles; code compiles locally.
  2. 09:47Pager alert: endpoint returns HTTP 500 for concurrent requests.
  3. 09:50Check logs: panic 'already borrowed: BorrowMutError' in user profile handler.
  4. 10:05Identify use of RefCell<Vec<User>> in shared state; concurrent requests cause runtime borrow conflict.
  5. 10:12Attempt quick fix: wrap in Mutex; compile error: 'cannot borrow data in a MutexGuard'.
  6. 10:25Restructure: move mutable state to actor model with actix::Actor; borrow checker passes.
  7. 10:40Deploy fix; monitor; no more panics.

We had a shared cache of recently updated user profiles stored in a RefCell<Vec<User>> inside an Actix web app state. The endpoint would read the cache (immutable borrow) and then, if not found, fetch from DB and push to cache (mutable borrow). Under low concurrency, it worked. Under load, two requests would hit the same code path: one held an immutable borrow while the other tried a mutable borrow, causing a runtime panic.

My first instinct was to replace RefCell with a Mutex. But then I got a compile error: 'cannot borrow data in a MutexGuard as mutable' because I was trying to hold the lock guard across an await point. The borrow checker caught a potential deadlock. I had to restructure to hold the lock only for the duration of the cache access, not across the DB query.

The real fix was to use an actor pattern: the cache lived inside an actix::Actor, and requests sent messages to it. This avoided holding any borrow across await points. The borrow checker was happy, and the runtime panics vanished. Lesson: don't fight the borrow checker; it's telling you about a real concurrency issue.

Root cause

RefCell with shared mutable state in async handlers caused runtime borrow conflict under concurrency.

The fix

Refactored to actor-based design: cache state is owned by an actor, accessed via message passing, eliminating shared mutable borrows.

The lesson

In async Rust, avoid RefCell for shared state; use actors or channels to manage mutable state without borrowing across await points.

( 08 )Understanding the Error Messages

The Rust compiler is exceptionally good at telling you exactly what went wrong. When you see 'cannot borrow `x` as mutable more than once at a time', look at the error note: it will say 'second mutable borrow occurs here' and point to the exact line. The first borrow is also marked. Your job is to ensure the first borrow ends before the second starts.

Another common message is 'cannot borrow `x` as immutable because it is also borrowed as mutable'. This happens when you have a mutable reference and then try to create an immutable one. The fix is often to reorder operations or clone the data. The compiler's suggestion to use `&` or `mut` is not always correct—read the full error context.

( 09 )Split Borrows: Borrowing Different Fields

Rust allows borrowing different fields of a struct simultaneously, as long as they are disjoint. For example, if you have `struct Point { x: i32, y: i32 }`, you can do `let a = &mut point.x; let b = &mut point.y;` because the borrow checker can prove they don't overlap. This is a powerful pattern to avoid cloning or refactoring.

However, this only works for direct fields. If you have a method that takes `&mut self`, you cannot call it while also having a borrow on a field—the method borrows the whole struct. The solution is to extract the logic into a free function that takes references to the specific fields.

( 10 )Interior Mutability with RefCell and Mutex

When you need to mutate data behind an immutable reference, interior mutability is the answer. `RefCell` provides runtime borrow checking: you call `borrow_mut()` to get a mutable reference, and if another borrow already exists, it panics. This moves the error from compile time to runtime—use with caution and only when you understand the trade-off.

`Mutex` is the thread-safe version. It blocks until the lock is acquired. In async code, never hold a Mutex lock across an await point—this can cause deadlocks. Use `tokio::sync::Mutex` instead, which is designed for async. The borrow checker will still enforce that you don't hold the guard across await if you use the standard Mutex incorrectly.

( 11 )Non-Lexical Lifetimes (NLL) and What They Don't Fix

Since Rust 2018 edition, the borrow checker uses non-lexical lifetimes (NLL). This means a borrow ends when it is last used, not at the end of the scope. This eliminates many false positives from the old lexical system. For example, you can now mutate a variable after its last use of an immutable reference.

But NLL does not fix fundamental aliasing violations. If you have two mutable references alive at the same time, NLL won't help. It also doesn't fix issues with closures that capture references—you still need to ensure the closure doesn't outlive the borrow. Always run `cargo fix --edition` to upgrade to NLL if you're on an older edition.

Frequently asked questions

Why does the compiler say 'cannot borrow `x` as mutable more than once' when I only have one mutable reference?

This often happens when you have a mutable reference that is still alive (even if not used) because of its scope. For example, if you create a mutable reference in an if-let block and then try to use the variable mutably after the block, the first borrow may still be considered alive. Use braces to limit the scope of the first borrow, or check that the first borrow is dropped before the second.

How do I fix 'cannot move out of borrowed content'?

This occurs when you try to move a value out of a reference (e.g., `let x = *ref_to_option;` where Option has ownership semantics). You cannot move out of a borrowed context because that would leave the original reference dangling. Instead, use `clone()` if you need an owned copy, or use `take()` on an Option to leave a None in its place.

Should I use Rc<RefCell<T>> or Arc<Mutex<T>>?

Use Rc<RefCell<T>> for single-threaded scenarios where you need shared mutable state. Use Arc<Mutex<T>> for multi-threaded scenarios. If you're in an async context, prefer tokio::sync::Mutex or actix::Actor to avoid blocking. Never use RefCell in multi-threaded code—it's not Send.

Why does my code compile with .clone() but not without?

Cloning creates an owned copy of the data, which removes the borrow. This is a valid fix when you don't need to share the original data. However, it can be expensive for large structs. Consider whether you can restructure to avoid the need for the second reference, or use a split borrow.