</>
Skip to content
Rust lessons (21/43)

Rust — Borrowing

Borrow instead of move

Passing ownership everywhere is exhausting. References let functions use values temporarily:

fn length(s: &String) -> usize {    // & = "borrow this"
    s.len()
}                                    // borrowed value NOT dropped

fn main() {
    let text = String::from("hello");
    let len = length(&text);         // lend it — ownership stays home
    println!("'{}' is {} chars", text, len);   // ✓ text still usable
}

&text creates a reference pointing at text without owning it. The function reads; the original survives.

Rule 1 — many shared borrows, OR…

Any number of read-only borrows may coexist:

let s = String::from("data");

let r1 = &s;
let r2 = &s;
let r3 = &s;

println!("{} {} {}", r1, r2, r3);   // ✓ three readers, zero conflicts

Rule 2 — …but only ONE mutable borrow

Writing requires exclusivity:

let mut s = String::from("hi");

let w1 = &mut s;
// let w2 = &mut s;      ❌ second mutable borrow — compile error!
w1.push_str(" there");

And a mutable borrow excludes ALL other borrows while alive:

let mut s = String::from("hi");
let r = &s;
let w = &mut s;          // ❌ can't borrow as mutable while r exists
println!("{}", r);

Why these rules? They make data races and use-after-modify bugs impossible at compile time. The classic bug they prevent:

// JavaScript happily lets this produce garbage:
list.push(list[0])  // while iterating…

Non-lexical lifetimes — borrows end early

A borrow ends at its LAST USE, not at scope end:

let mut s = String::from("hello");

let r1 = &s;
println!("{}", r1);       // ← last use of r1: borrow ENDS here

let w = &mut s;           // ✓ fine now — nothing borrows anymore
w.push('!');

Older compilers scoped to the whole block; modern Rust (NLL) tracks actual usage. This is why code that "should" fail sometimes compiles.

Dangling references are impossible

fn dangle() -> &String {     // ❌ compile error
    let s = String::from("hi");
    &s                        // s dies at end of fn → reference would dangle
}

The borrow checker refuses any reference outliving its data. C/C++ let this through; Rust makes it unrepresentable.

Slices — borrowed views into data

let s = String::from("hello world");

let hello = &s[0..5];        // &str — view of first 5 bytes
let world = &s[6..];

// string literals ARE slices:
let literal: &str = "already a slice";

Slices explain why APIs prefer &str: accepting &str accepts both owned strings (&s) and literals.

The idiomatic signature

// ✗ forces callers to own:
fn print(s: String) { … }

// ✓ accepts String AND &str literals:
fn print(s: &str) { … }

print(&my_string);   // borrow of owned
print("literal");    // works directly

Fixing the top-3 borrow errors

Error messageUsual fix
cannot borrow as mutableadd mut to variable + change reader borrows to end earlier
value borrowed after movepass &value, or clone if truly needed
missing lifetime specifierrestructure so output clearly derives from inputs (lifetimes lesson)

Mini Practice

  1. Rewrite consume() from the ownership lesson as borrow-based.
  2. Create three simultaneous &borrows; print all.
  3. Trigger double-mutable-borrow error; fix by scoping the first borrow.
  4. Write longest() taking two &str and returning the longer one.
  5. Change a String parameter to &str; update call sites.

Next: arrays →

Related Topics

Frequently Asked Questions about Borrowing

What is Borrowing in Rust?

Borrowing is a fundamental concept in Rust. This lesson explains it step by step with clear examples, making it easy for beginners to understand.

How do I learn Borrowing?

Start by reading the explanation above, then try the code examples. Practice by modifying the examples and experimenting with different values. Hands-on practice is the best way to learn Borrowing.

Why is Borrowing important in Rust?

Borrowing is essential for Rust development. Understanding this concept will help you write better code and solve real-world problems more effectively.