Borrowing instead of moving

On Ownership, passing a String or Vec to a function moved it, and the caller lost it. Most functions only need to look at a value, or change it in place, and then let the caller carry on. Borrowing is using a value through a reference, created with &, without taking ownership of it: the owner stays the owner, and the value is not dropped when the borrower is done.

fn total_kwh(readings: &Vec<u32>) -> u32 {
    let mut total = 0;
    for kwh in readings {
        total += kwh;
    }
    total
}

fn main() {
    let readings = vec![350, 412, 120];
    let total = total_kwh(&readings);      // lend readings to the function
    println!("total {total} kWh over {} readings", readings.len());
}
total 882 kWh over 3 readings

&readings creates a reference: on a 64-bit machine an 8-byte address of the vector, nothing more. The type &Vec<u32> in the signature says "a reference to a vector", so the caller must lend, and the function cannot keep or free the vector. The & is visible at the call site, so a reader of main knows that readings is only lent out.

You rarely have to write * to follow a reference when reading: method calls (readings.len()) and println! follow references automatically, and a for loop over &Vec<u32> hands out &u32 values that += accepts. A comparison needs the value itself: *kwh > 400, as in a loop further down.

Shared references

A shared reference, written &T, gives read-only access to a value. Any number of shared references to the same value may exist at the same time, and shared references are Copy, so handing one to several functions is free. Trying to change a value through one is a compile error:

fn add_reading(readings: &Vec<u32>, kwh: u32) {
    readings.push(kwh);
}

fn main() {
    let readings = vec![350, 412];
    add_reading(&readings, 120);
    println!("{readings:?}");
}
example.rs:2:5: error[E0596]: cannot borrow `*readings` as mutable, as it is behind a `&` reference: `readings` is a `&` reference, so it cannot be borrowed as mutable
help: consider changing this to be a mutable reference
  |
1 | fn add_reading(readings: &mut Vec<u32>, kwh: u32) {
  |                           +++

Mutable references

A mutable reference, written &mut T, gives temporary exclusive access to a value, including the right to change it. You can only take one from a variable that is itself declared mut, and the caller writes &mut too, so a change through a function call is never a surprise. To assign to the value behind a reference, follow it with *:

fn add_reading(readings: &mut Vec<u32>, kwh: u32) {
    readings.push(kwh);
}

fn bump(counter: &mut u32) {
    *counter += 1;                  // * reaches the value behind the reference
}

fn main() {
    let mut readings = vec![350, 412];
    let mut rejected = 0;
    add_reading(&mut readings, 120);
    bump(&mut rejected);
    println!("{readings:?}, rejected {rejected}");
}
[350, 412, 120], rejected 1

This is how Rust does what C does with "pass a pointer so the function can change the caller's variable" (see Pointers), with the difference that the compiler knows which functions may change what. A &mut parameter in a signature is a clear statement: this function changes its argument.

Many readers or one writer

The central borrowing rule fits on one line. At any moment, a value can have either any number of shared references or exactly one mutable reference, but not both. This rule is often called aliasing XOR mutation: a value can be aliased (reachable through several references) or mutated, never both at once, which rules out a whole family of bugs where code reads data that someone else is changing underneath it.

Two situations side by side. On the left, three shared references point to the same value readings: any number of readers, and nobody may change it. On the right, a single mutable reference points to the value: exactly one writer, and no other reference may exist at the same time. shared: &T & & & readings many readers, no writer mutable: &mut T &mut readings one writer, no other reference
The two legal situations. Mixing them, or having two writers, does not compile.

Two mutable borrows of the same vector at once:

fn main() {
    let mut readings = vec![350, 412];
    let first = &mut readings;
    let second = &mut readings;
    first.push(120);
    second.push(95);
}
example.rs:4:18: error[E0499]: cannot borrow `readings` as mutable more than once at a time: second mutable borrow occurs here

The more instructive case is a shared borrow of one element while the vector grows. It looks harmless, and in C it is a real bug:

fn main() {
    let mut readings = vec![350, 412];
    let first = &readings[0];
    readings.push(120);
    println!("first: {first}");
}
example.rs:4:5: error[E0502]: cannot borrow `readings` as mutable because it is also borrowed as immutable: mutable borrow occurs here
error[E0502]: cannot borrow `readings` as mutable because it is also borrowed as immutable
 --> example.rs:4:5
  |
3 |     let first = &readings[0];
  |                  -------- immutable borrow occurs here
4 |     readings.push(120);
  |     ^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
5 |     println!("first: {first}");
  |                       ----- immutable borrow later used here

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0502`.

Why refuse it? push may find the buffer full, allocate a bigger one, copy the elements and free the old buffer, exactly like C's realloc. first would then point into freed memory: a use-after-free. In C this compiles and usually works in a quick test, because the buffer happens to have room; it fails only when the vector grows. Rust does not try to work out whether this particular push reallocates. The rule is simpler and always holds: while a shared reference is in use, nobody changes the value.

The same rule rejects changing a collection while looping over it, a bug that Python reports at run time for dictionaries ("dictionary changed size during iteration") and Java with a ConcurrentModificationException. Here the compiler's help line even names the usual fixes:

fn main() {
    let mut readings = vec![350, 412, 120];
    for kwh in &readings {
        if *kwh > 400 {
            readings.push(*kwh / 2);
        }
    }
    println!("{readings:?}");
}
example.rs:5:13: error[E0502]: cannot borrow `readings` as mutable because it is also borrowed as immutable: mutable borrow occurs here
  = help: consider using an index-based loop instead, or collecting modifications into a separate collection

Across threads, the same rule is what prevents data races: two threads cannot both hold a &mut to one value, so shared data must be protected by a lock or similar type (see Concurrency and race conditions).

The borrow checker

The borrow checker is the part of the Rust compiler that checks every reference in a program against the ownership and borrowing rules: that no reference is used after its value is gone, and that no value is changed while it is borrowed elsewhere. It works entirely at compile time; the compiled program contains no checks and no reference counts, and a reference costs what a C pointer costs.

A borrow does not last until the end of the block. With non-lexical lifetimes (NLL), the borrow checker considers a reference alive only from where it is created to where it is last used. Move the println! from the E0502 example above the push, and the program compiles:

fn main() {
    let mut readings = vec![350, 412];
    let first = &readings[0];
    println!("first: {first}");     // last use of first: the borrow ends here
    readings.push(120);
    let a = &readings;
    let b = &readings;              // any number of shared borrows at once
    println!("{} readings, {} readings", a.len(), b.len());
}
first: 350
3 readings, 3 readings

The borrow checker is conservative: it rejects every program that breaks the rules, and also some programs that are actually fine but that it cannot prove fine. As of October 2026, a next iteration of the borrow checker (the project known as Polonius) is enabled on the nightly channel, announced in August 2026, to accept more such programs; stable Rust still uses the current one. When it refuses correct-looking code, the fixes on this page usually help, and the error message almost always points at the three places that matter: where the borrow starts, where the conflicting use is, and where the borrow is used later.

No dangling references

A dangling reference is a reference to memory whose value has already been dropped or moved away, the Rust name for C's dangling pointer. The classic way to create one in C is to return the address of a local variable. In Rust it does not compile:

fn latest_label(meter: &str) -> &String {
    let label = format!("{meter}: 350 kWh");
    &label
}

fn main() {
    println!("{}", latest_label("M-30004"));
}
example.rs:3:5: error[E0515]: cannot return reference to local variable `label`: returns a reference to data owned by the current function

label is dropped when the function returns, so a reference to it would point to freed memory. The fix is to return the String itself, which moves ownership to the caller:

fn latest_label(meter: &str) -> String {
    format!("{meter}: 350 kWh")     // return the String itself: ownership moves out
}

fn main() {
    println!("{}", latest_label("M-30004"));
}
M-30004: 350 kWh

The same protection works inside a function. A reference stored in a variable that lives longer than the value it points to is refused with E0597, "does not live long enough":

fn main() {
    let latest;
    {
        let reading = String::from("350 kWh");
        latest = &reading;
    }
    println!("{latest}");
}
example.rs:5:18: error[E0597]: `reading` does not live long enough: borrowed value does not live long enough

How the compiler tracks how long each reference may live, and the cases where you have to tell it, is the subject of Lifetimes.

Slices are borrows

A slice is a reference to a run of elements inside something else: &str is a borrowed piece of UTF-8 text, &[u32] a borrowed piece of an array or vector. Slicing copies nothing. A slice reference is a pointer plus a length, 16 bytes on a 64-bit Linux machine (ARM64), twice the size of an ordinary reference. Functions that only read text or numbers should take slices, because then they accept every kind of argument:

fn meter_id(line: &str) -> &str {
    match line.find(';') {
        Some(end) => &line[..end],
        None => line,
    }
}

fn total_kwh(readings: &[u32]) -> u32 {
    readings.iter().sum()
}

fn main() {
    let line = String::from("M-30004;350;fixed");
    println!("meter {}", meter_id(&line));          // &String works where &str is expected
    println!("literal {}", meter_id("M-30007;412;dynamic"));

    let readings = vec![350, 412, 120, 95];
    println!("all {}", total_kwh(&readings));       // &Vec<u32> works where &[u32] is expected
    println!("first two {}", total_kwh(&readings[..2]));
    println!("array {}", total_kwh(&[100, 200]));
    println!("sizes: &str {} bytes, &[u32] {} bytes, &u32 {} bytes",
        size_of::<&str>(), size_of::<&[u32]>(), size_of::<&u32>());
}
meter M-30004
literal M-30007
all 977
first two 762
array 300
sizes: &str 16 bytes, &[u32] 16 bytes, &u32 8 bytes

meter_id returns a slice of its argument: the first seven bytes of the same text, not a copy. A slice is a borrow like any other, so while it is in use, the text it points into cannot change:

fn meter_id(line: &str) -> &str {
    match line.find(';') {
        Some(end) => &line[..end],
        None => line,
    }
}

fn main() {
    let mut line = String::from("M-30004;350;fixed");
    let id = meter_id(&line);
    line.clear();                   // reuse the buffer for the next line
    println!("meter {id}");
}
example.rs:11:5: error[E0502]: cannot borrow `line` as mutable because it is also borrowed as immutable: mutable borrow occurs here

In C the equivalent, a char * into a buffer that is then overwritten, prints whatever the next line contains. Copy the id out with id.to_string() before clearing the line, or print it first. Slices of strings and the byte-versus-character question are covered on Collections and Strings.

Working with the borrow checker

Most borrow errors in everyday code come from holding a reference longer than needed. A handful of patterns solve nearly all of them:

struct Meter {
    id: String,
    readings: Vec<u32>,
}

fn main() {
    let mut readings = vec![350, 412, 120];

    // 1. Copy small values out instead of keeping a reference.
    let first = readings[0];
    readings.push(95);

    // 2. Collect the changes first, apply them after the loop.
    let mut extra = Vec::new();
    for kwh in &readings {
        if *kwh > 400 {
            extra.push(*kwh / 2);
        }
    }
    readings.extend(extra);

    // 3. Borrow different fields of a struct separately.
    let mut meter = Meter { id: String::from("M-30004"), readings };
    let id = &meter.id;
    meter.readings.push(120);
    println!("{id}: first {first}, now {:?}", meter.readings);
}
M-30004: first 350, now [350, 412, 120, 95, 206, 120]
  • Copy or index instead of borrowing. A u32 copied out of the vector is independent of it. An index (usize) is not a borrow either, and is checked when used.
  • Separate reading from changing. First collect what must change, then change it, as in the loop above.
  • Borrow fields, not the whole struct. The compiler tracks fields separately inside one function, so &meter.id and a change to meter.readings can coexist. A method that takes &mut self borrows the whole struct, so the same split across method calls does not work.
  • Own the data when a borrow gets complicated. Storing a String instead of a &str, or cloning a small value, often makes a design simpler at a cost you will never measure.
  • Read the whole error. The three labels (borrow starts, conflicting use, later use) show exactly which lines to reorder.

Sometimes shared, changeable data is what the design really needs, for example a cache that several parts of the program update. Rust has types for that, which move the borrowing check to run time or use locks: see Smart Pointers and Concurrency.