What is borrowing in Rust

Borrowing in Rust lets you access data via references without taking ownership, enforcing strict rules to prevent data races and ensure memory safety.

Borrowing: Using data without stealing it

You're building a game loop. The renderer needs to draw the player. The physics engine needs to update the player's velocity. You have one Player struct on the stack. You can't hand ownership to the renderer because the physics engine still needs it later. You can't clone the player every frame; that's slow and the renderer would draw a ghost while the physics engine moves the real player. You need a way to let functions peek at or modify the player without stealing it.

That's borrowing. Borrowing lets you pass references to data while the original owner keeps the value alive. The compiler enforces strict rules on how you borrow to prevent data races and dangling pointers. You get zero-cost access without the overhead of copying or the risk of memory errors.

The rules of the road

Borrowing follows two simple rules. Break either one and the compiler rejects your code.

  1. You can have any number of immutable borrows (&T) or exactly one mutable borrow (&mut T) at a time.
  2. Borrows must always be valid. A reference cannot outlive the data it points to.

Think of borrowing like lending a book. You can lend the book to ten friends for reading. They can all read at once. No problem. You can lend the book to one friend for editing. But if one friend is editing, no one else can read or edit. The owner keeps the book; the borrower just has temporary access.

Immutable borrows are read-only. The compiler guarantees that while an immutable borrow exists, no one can change the data. This prevents readers from seeing half-written values. Mutable borrows allow reading and writing, but the compiler guarantees exclusive access. No one else can touch the data while a mutable borrow is active.

Minimal example

This example shows immutable and mutable borrows in action. The owner keeps the String while functions borrow it.

/// Prints the length of a string without taking ownership.
fn print_len(s: &String) {
    // s is an immutable reference. We can read it.
    println!("Length: {}", s.len());
}

/// Appends text to a string via mutable borrow.
fn append_text(s: &mut String) {
    // s is a mutable reference. We can modify it.
    s.push_str(" world");
}

fn main() {
    // Owner creates the String on the heap.
    let mut s = String::from("hello");

    // Borrow immutably. Multiple calls allowed.
    print_len(&s);
    print_len(&s);

    // Borrow mutably. Exclusive access required.
    // The immutable borrows above are done.
    append_text(&mut s);

    println!("{}", s);
}

Convention aside: When you borrow a String, you can write &s or s.as_str(). Both compile. The community prefers s.as_str() in function signatures and arguments because it borrows the inner string slice, not the String wrapper. This makes your code work with &String and &str interchangeably.

What the compiler tracks

The compiler tracks borrows using a system called the borrow checker. It doesn't just look at scopes; it looks at usage. This feature is called Non-Lexical Lifetimes (NLL). The compiler marks a borrow as finished when you stop using it, even if the variable is still in scope.

In the example above, print_len(&s) creates an immutable borrow. The borrow ends immediately after the function returns. You don't need to manually drop the reference. The compiler sees that print_len is done with s and allows append_text to borrow mutably right after.

If you tried to hold a reference and then mutate the owner, the compiler would stop you.

fn main() {
    let mut s = String::from("hello");
    let r1 = &s;
    let r2 = &s;
    
    // r1 and r2 are still alive here.
    // The compiler sees active immutable borrows.
    let r3 = &mut s; // ERROR: E0502
    
    println!("{}", r1);
}

The compiler rejects this with E0502 (cannot borrow as mutable because it is also borrowed as immutable). You have active immutable borrows. You can't get a mutable borrow until r1 and r2 are done. Move the println calls before the mutable borrow to fix this.

The borrow checker isn't guessing. It's enforcing a contract. If you can't prove exclusive access, you don't get it.

Realistic scenario

Borrowing shines when you work with structs. You can borrow the whole struct or specific fields. This avoids cloning large data structures.

struct User {
    name: String,
    email: String,
}

/// Checks if the email contains an @ symbol.
fn validate_email(user: &User) -> bool {
    user.email.contains('@')
}

/// Updates the user's name by adding a prefix.
fn add_prefix(user: &mut User, prefix: &str) {
    let new_name = format!("{} {}", prefix, user.name);
    user.name = new_name;
}

fn main() {
    let mut user = User {
        name: String::from("Alice"),
        email: String::from("alice@example.com"),
    };

    // Validate first. Immutable borrow.
    if validate_email(&user) {
        println!("Email is valid.");
    }

    // Now mutate. Mutable borrow.
    // The immutable borrow from validate_email is done.
    add_prefix(&mut user, "Dr.");

    println!("User: {}", user.name);
}

This code borrows the User struct. validate_email takes &User. It can read any field. add_prefix takes &mut User. It can modify fields. The compiler ensures that validate_email finishes before add_prefix starts.

Pitfalls and errors

Borrowing errors are common when you start. The compiler messages are precise. Learn to read them.

E0502: Cannot borrow as mutable because it is also borrowed as immutable.

This happens when you hold an immutable reference and try to get a mutable one.

fn main() {
    let mut s = String::from("hello");
    let r = &s;
    
    // r is still alive.
    s.push_str(", world"); // ERROR: E0502
    
    println!("{}", r);
}

The fix is to use r before mutating s. The compiler tracks the last use of r. If you move the println before the mutation, the code compiles.

E0505: Cannot move out of borrowed content.

This happens when you try to move a value that is currently borrowed.

fn main() {
    let s = String::from("hello");
    let r = &s;
    
    // s is borrowed by r.
    let _owned = s; // ERROR: E0505
    
    println!("{}", r);
}

You can't move s because r points to it. If s moves, r becomes a dangling pointer. Drop r before moving s, or clone s instead of moving it.

E0507: Cannot move out of borrowed content.

Similar to E0505, but often happens inside functions when you try to extract owned data from a reference.

fn take_string(s: &String) {
    // You can't take ownership of the String inside s.
    let _owned = *s; // ERROR: E0507
}

References don't own the data. You can't dereference a &String to get a String. You can clone it, or borrow the inner str with s.as_str().

E0597: Borrowed value does not live long enough.

This happens when a reference outlives the data it points to.

fn main() {
    let reference;
    {
        let s = String::from("hello");
        reference = &s; // ERROR: E0597
    }
    // s is dropped here. reference is dangling.
    println!("{}", reference);
}

The inner scope ends, s is dropped, and reference points to freed memory. Rust prevents this at compile time. Move s outside the scope, or don't hold the reference across the scope boundary.

Borrows are temporary. The owner is permanent until the end. Never let a reference outlive the data it points to.

Borrowing vs Copying

Some types implement the Copy trait. i32, bool, f64, and tuples of Copy types are Copy. When you pass a Copy type, Rust copies the bits. Borrowing a Copy type works, but the compiler often optimizes it to a copy anyway.

fn double(x: i32) -> i32 {
    x * 2
}

fn main() {
    let x = 5;
    let y = double(x);
    println!("x: {}, y: {}", x, y);
}

Here, x is copied into double. You can still use x afterward. This isn't borrowing; it's copying. The cost is negligible for small types.

Convention aside: Don't over-borrow Copy types. Passing &i32 is valid but unnecessary noise. Pass i32 directly. The compiler handles the register allocation. Reserve borrowing for types that are expensive to clone, like String, Vec, or large structs.

When to borrow, move, or clone

Choosing the right access pattern matters for performance and correctness. Use the parallel structure below to decide.

Use &T when you need to read data and the owner must keep using it. This is the default for function arguments. Use &mut T when you need to modify data in place and no other references exist. Use ownership (T) when the function takes full responsibility for the value, such as consuming a buffer or moving data into a collection. Use .clone() when you need independent copies of data, accepting the performance cost. Reach for Rc<T> or Arc<T> when multiple owners are required; borrowing cannot solve multi-owner scenarios.

Default to borrowing. Take ownership only when you have to. Clone only when you must.

Where to go next