Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Borrowing in One Page

If you only remember one thing: a borrow lets a function look at your value without taking it over, and the compiler guarantees the value will still be valid while the borrow lives.

The problem borrowing solves

We want this to work:

fn main() {
    let name = String::from("Ada");
    print_name(name);
    println!("{name}");   // we'd like to still use name here
}

fn print_name(n: String) {
    println!("hello, {n}");
}

It does not work. print_name(name) moves name into the function. By the time println! on the next line runs, name is gone.

We could fix it by cloning, but cloning a string means copying the bytes, which we do not need. What we actually want is “let the function look at the value, but keep ownership here.”

That is a borrow. You write it with &:

fn main() {
    let name = String::from("Ada");
    print_name(&name);
    println!("{name}");   // still valid — we only lent the name out
}

fn print_name(n: &String) {
    println!("hello, {n}");
}

▶ Run this in the Rust Playground

Plain English

A borrow is "please read this, do not take it". The & is the way you hand the thing over to someone to look at, not to keep.

Two kinds of borrow

There are two borrow shapes in Rust, and they follow one rule between them.

The aliasing rule

Many readers OR one writer. Never both at once.

&T — shared (read-only) value many readers, all at once, all fine. &mut T — exclusive (read & write) value one writer, alone, only while writing.
At any moment you can have many &T readers or exactly one &mut T writer. This is the borrow rule.
  • &T — a shared borrow. Read-only. Many allowed at once.
  • &mut T — an exclusive borrow. Read and write. Exactly one allowed at a time, and no &T readers can exist while it does.

That rule is the whole borrow checker. Everything else follows from it.

The rule in code

This compiles. Many readers, no writers:

fn main() {
    let s = String::from("rust");
    let a = &s;
    let b = &s;
    println!("{a} {b} {s}");
}

This also compiles. One writer, no readers:

fn main() {
    let mut s = String::from("rust");
    let w = &mut s;
    w.push_str("!");
    println!("{w}");
}

This does not compile. A reader and a writer at the same time:

fn main() {
    let mut s = String::from("rust");
    let r = &s;
    let w = &mut s;   // error: cannot borrow `s` as mutable because it is also borrowed as immutable
    println!("{r} {w}");
}

▶ Run this in the Rust Playground

Read the error when it shouts at you. It will tell you, to the character, which borrow was already live and what you tried to do.

Why the rule

If one person is reading your notebook and another person starts erasing and rewriting it, what the first person sees is nonsense. Rust catches that at compile time for every single piece of data you own. Data races — the bug category that eats entire engineering quarters — simply cannot compile.

A passing note on lifetimes

Every borrow lives for a certain slice of time — from the moment it is taken to the last moment it is used. Rust tracks these slices automatically. You will not usually annotate them. When you do need to, the notation looks like &'a str and it is covered in Part 3.

For now, take the following on faith: if the value you borrowed from is dropped, any borrow of it stops being valid, and the compiler will refuse to let you use it. That is another bug class — use after free — quietly removed.

String vs &str — the first consequence

This is where Rust’s two string types finally make sense.

  • String — an owning string. A heap-allocated, growable buffer that you own. Like std::string in C++ or StringBuilder.
  • &str — a borrowed view into some text that lives elsewhere. Pronounced “string slice”. Like a read-only window.

When you write "hello" in Rust source, you get a &str — a borrowed view of a literal baked into the program. When you call String::from("hello"), you allocate your own owned copy. Any function that just wants to read some text should take &str.

fn greet(name: &str) {
    println!("hello, {name}");
}

fn main() {
    greet("Ada");                       // passes a &str directly
    let name = String::from("Ben");
    greet(&name);                       // &String auto-coerces to &str
}

▶ Run this in the Rust Playground

Plain English

Default habit: your functions take &str, not String. Callers can pass either — Rust will do the coercion for you. You keep the flexibility, they keep ownership of their data.

Step through the lending version and watch the reference point back at the owner — and the owner survive the call.

Interactive simulation (requires JavaScript): passing &name lends the String to print_name, whose parameter points back at the owner; when the function returns the loan ends and name is still valid.

## Try this
Five-minute exercises
  1. Write a function fn length(s: &str) -> usize that returns the number of bytes in the string. Call it from main.
  2. Write a function fn shout(s: &mut String) that appends "!" to its argument. Call it on a let mut msg = String::from("hi"); and print the result.
  3. Write a program that holds both an &s and an &mut s at the same time. Read the error. It will tell you exactly which line to move.

Now for the other idea Rust is famous for — how errors and missing values are just ordinary data.

Option and Result →