Fix Common Errors

Rust E0505: Cannot Move Out of X Because It Is Borrowed

E0505 fires when you move a value while a borrow is still live. Learn how NLL scopes borrows, and the four refactors that resolve it without cloning.

When you move a value in Rust while a reference to it is still in use, the compiler stops you with E0505: cannot move out of X because it is borrowed. This error often surprises developers who understand basic borrowing but haven't internalized non-lexical lifetimes (NLL)—Rust's mechanism for tracking when borrows actually end. The good news: reordering your code to finish using the reference before the move often fixes rust e0505 instantly, without restructuring.

This error is related to but distinct from E0507 (cannot move out of borrowed content). Here's the key difference: E0505 fires when you move a value while an active borrow still exists. E0507 fires when you try to move out of something behind a reference—the reference itself is the barrier. Knowing which one you hit tells you exactly what to fix.

E0505 vs E0507: The Distinction

Both errors involve moves and borrows, but the mechanics differ:

E0505 (this post):

let mut x = String::from("hi");
let r = &x;
let y = x;      // ERROR E0505: cannot move out of `x` because it is borrowed
println!("{r}");

You own x, you borrowed it into r, and then you tried to move x—the borrow is still live at that point. The borrow ends at its last use (println!), which comes after the move.

E0507 (different error):

fn take_field(reference: &Config) -> String {
    reference.name  // ERROR E0507: cannot move out of borrowed reference
}

reference is already borrowed (&Config), so you can't move out of it at all. The barrier is the reference itself, not a later use.

The fix for E0505 is often simple: reorder. The fix for E0507 is restructuring (clone, return a reference, or take ownership at the call site). If you're seeing E0505, you're in the simpler case.

Understanding the Rule and Non-Lexical Lifetimes

A borrow must remain valid for its entire lifetime—the span of code where the reference is used. The critical insight is that a borrow's lifetime ends at its last use, not at the end of its scope. This is called non-lexical lifetimes (NLL).

Before NLL (older Rust), a borrow lasted until the end of the scope it was declared in. That meant this would fail:

let mut x = 5;
let r = &x;
println!("{r}");
let y = x;  // Would fail in pre-NLL Rust, but...

With NLL, the reference r ends at the println! line—that's its last use. After that, the borrow is dead, and x is free to move. But if you wrote:

let r = &x;
let y = x;      // E0505: move happens before last use
println!("{r}"); // Last use is here

Now the move happens before the last use, violating the rule. The borrow must be valid from creation to last use; a move in between breaks that contract.

This is why reordering is such a powerful fix: you're making the last use happen before the move.

Non-lexical lifetimes (NLL): A borrow's lifetime is determined by its first use (where it's created) and its last use (where the reference is consumed or read). It is not tied to the scope it's declared in. Reordering to finish the last use before the move eliminates the E0505.

The Canonical E0505 Reproduction

Here's the minimal example the compiler will show you:

let mut x = String::from("hello");
let r = &x;
let y = x;      // E0505: cannot move out of `x` because it is borrowed
println!("{r}");

Compiler output:

error[E0505]: cannot move out of `x` because it is borrowed
 --> src/main.rs:4:9
  |
3 | let r = &x;
  | -- borrow of `x` occurs here
4 | let y = x;
  | ^ move out of `x` occurs here
5 | println!("{r}");
  | - borrow later used here
  |
  = note: borrow later used here

The error message is telling you: "the borrow created on line 3 is used again on line 5, so you can't move on line 4."

Fix 1: Reorder—Move After the Borrow Ends

The simplest fix is to finish using the borrow before the move:

Before:

let mut x = String::from("hello");
let r = &x;
let y = x;           // E0505
println!("{r}");

After:

let mut x = String::from("hello");
let r = &x;
println!("{r}");     // Last use of r
let y = x;           // OK: borrow is dead now

The borrow r ends at the println!, so when you move x on the next line, there are no live references. No restructuring needed—just reorder. This is the fix NLL was designed to enable.

Fix 2: Scope the Borrow Early

If you can't reorder (e.g., the reference is needed in multiple branches), wrap it in a block to end it early:

Before:

let mut data = vec![1, 2, 3];
let r = &data;
println!("{r:?}");
let owned = data;    // E0505

After:

let mut data = vec![1, 2, 3];
{
    let r = &data;
    println!("{r:?}");
}
let owned = data;    // OK: r is dropped at block end

The borrow now ends at the closing brace, before the move. This is useful when the reference is part of a larger scope and you want to drop it early without reordering the whole block.

Fix 3: Clone the Value

If you need both the reference and the moved value, clone:

Before:

let mut x = String::from("hello");
let r = &x;
let y = x;           // E0505
println!("{r}, {y}");

After:

let mut x = String::from("hello");
let r = &x;
let y = x.clone();   // OK: y is a copy
println!("{r}, {y}");

Cloning is the escape hatch—it copies the data so you have two owned values. The cost is a heap allocation (for String) or a full copy (for smaller types). Use this when the value is small, you're only doing it once, or the API you're calling requires owned data.

Don't default to cloning to silence E0505—reordering is almost always better. Clone when: (1) you genuinely need two owned copies, (2) the cost is negligible, or (3) restructuring would be more complex than the copy.

Fix 4: Borrow Instead of Move

If the code that consumes the value doesn't actually need ownership, pass a reference:

Before:

let data = vec![1, 2, 3];
let r = &data;
println!("{r:?}");
let len = consume_owned(data);  // E0505: move while r is live

After:

let data = vec![1, 2, 3];
let r = &data;
println!("{r:?}");
let len = consume_ref(&data);   // OK: pass reference, keep ownership

Change the function signature:

// Before
fn consume_owned(v: Vec<i32>) -> usize { v.len() }

// After
fn consume_ref(v: &Vec<i32>) -> usize { v.len() }

If the function only reads the data, it doesn't need to own it. Borrowing keeps the original value alive and available to the reference.

Fix 5: Restructure Ownership with Rc, Arc, or mem::take

For complex cases—shared ownership, thread-safe sharing, or moving out of a mutable container while keeping the container alive—use higher-level abstractions:

Rc for single-threaded sharing:

use std::rc::Rc;

let x = Rc::new(String::from("hello"));
let r = Rc::clone(&x);  // Cheap copy of the reference
let y = (*x).clone();    // Only clone if truly needed
println!("{:?}", r);

std::mem::take to move out while leaving a valid default:

let mut container = Some(String::from("hello"));
let r = &container;

if let Some(s) = std::mem::take(&mut container) {
    let owned = s;  // No move conflict; take swapped in None
    println!("{:?}", r);
    container = Some(owned);
}

These patterns are heavier but necessary when you need shared ownership or complex lifetimes. They're rare fixes for E0505; if you reach for them, first verify that reordering or restructuring the function signatures won't work.

Real-World Scenarios

Borrowing a collection, then moving it:

let mut items = vec![1, 2, 3];
let first = &items[0];
items = process(items);  // E0505: move while first is borrowed
println!("{first}");

Fix: print first before the reassignment, or pass &items to process.

Closures and move semantics:

let x = String::from("data");
let r = &x;
let f = move || println!("{x}");  // E0505: can't move x while r is alive
println!("{r}");
f();

Fix: print r first, then create and call the closure.

Struct fields and borrows:

struct Wrapper(String);
let w = Wrapper(String::from("data"));
let r = &w.0;
let s = w;  // E0505: move struct while field is borrowed
println!("{r}");

Fix: use r before moving w, or restructure to avoid partial borrows.

Tracing Production Errors After Compile Time

E0505 is caught at compile time, so you'll never see it crash in production. But once your Rust service is live, other errors will arise—panics, logic bugs, and hard-to-reproduce concurrency issues. Learning to debug production errors with error tracking best practices ensures you catch and fix runtime issues as fast as you fixed E0505 at the compiler.

The same discipline applies: understand ownership and flow of data through your service, log errors consistently, and trace the conditions that led to each failure.

Start tracking errors in minutes

Once your Rust service is live, track panics and runtime errors with LightTrace—fully Sentry-compatible error tracking that gives you the context you need to fix fast.

Fix your next production error faster

Point any Sentry SDK at LightTrace — free up to 5,000 events/month.