Chapter 6

Borrowing and references

👋 Anyone can read and edit this exercise. Sign up to save your progress.

Why are Rust developers so frugal? They prefer to borrow.

The functions chapter had a nuisance baked in. Pass a String to a function and the move rules say you've handed it over. Want to use it afterwards? Too bad, it's gone. Returning it back out works but gets tedious fast, especially when the function only needed to read the value.

Borrowing is the fix. A borrow lets a function use a value without taking ownership of it: &value for a shared, read-only borrow, and &mut value for an exclusive, writable one.

fn length(s: &String) -> usize { s.len() }   // borrows, doesn't take

let s = String::from("rust");
let n = length(&s);    // lend s out for the call
println!("{s}");       // s still owns the data

The caller keeps ownership the whole time. The function gets temporary access and gives it back when it returns.

The one rule

Borrows come with a single rule, and the borrow checker enforces it everywhere:

At any moment you can have either any number of shared & references, or exactly one &mut reference. Never both at once.

A shared reference promises the data won't change while you're looking at it. A mutable reference promises nobody else is looking while you write to it. Allow both at once and you'd have someone reading a value halfway through someone else's change, which is the classic data race. Rust rules that out at compile time instead of trusting you to get the locking right.

When the compiler rejects your code with a borrowing error, this rule is the first thing to check.

Borrowing without taking

Most of the time a function only needs to look at a value, not own it. That's a shared borrow, written &T in the signature. The caller keeps ownership; the callee gets temporary read-only access.

This function takes &str rather than &String. &str is the universal "borrowed string slice" type: a string literal is already a &'static str, and &String automatically coerces to &str, so &str parameters accept both without forcing the caller to convert. Reach for &str by default when you're just reading.

The body is a one-liner: call .len() on the slice. The point of the exercise is the signature: notice that after the call, the caller's s is still usable in the test below.

Useful from the standard library

  • str::len is the byte length of the slice. The chapter on strings covers why that's not the same as a character count.
  • The "deref coercion" from &String to &str is what lets the test pass &s directly. No .as_str() needed.
Exercise 1 of 3
Open in Web Editor

Results

    Compiler / runtime output
    
                
    Reveal the full solution Spoiler: the complete answer
    /// Borrows a string reference without taking ownership.
    /// The original string remains valid after this function returns.
    fn borrow_string(s: &str) -> usize {
        s.len()
    }
    
    #[test]
    fn test_borrow_string() {
        let s = "The Matrix has you";
        let len = borrow_string(s);
        // s is still valid here because we only borrowed it
        assert_eq!(len, 18);
        assert_eq!(s, "The Matrix has you"); // Still here!
    }
    

    Mutable borrows

    Sometimes you want to modify a value in place without taking ownership of it. That's a mutable borrow: &mut T. The caller still owns the value, but the callee gets exclusive write access for the duration of the call.

    Two things to notice in the signature:

    1. The parameter is &mut String, not &mut str. We need the owned String because growing it (with push_str) may reallocate; a bare string slice has a fixed length.
    2. There's no return value. The mutation happens through the reference and is visible to the caller after the call returns.

    On the call site (see the test): the caller has to write &mut s explicitly, and s itself has to have been declared let mut s = .... Mutability is opt-in at every layer.

    Useful from the standard library

    • String::push_str works on &mut String exactly the same way as on an owned String. The compiler reaches through the reference for you.
    • String::push is the single-char version, in case you want to append one character at a time.
    Exercise 2 of 3
    Open in Web Editor

    Results

      Compiler / runtime output
      
                  
      Reveal the full solution Spoiler: the complete answer
      /// Takes a mutable reference to modify the string in place.
      /// The &mut allows us to change the string's contents.
      fn mutate_string(s: &mut String) {
          s.push_str(" - now with extra crab");
      }
      
      #[test]
      fn test_mutate_string() {
          let mut s = String::from("Ferris");
          mutate_string(&mut s);
          assert_eq!(s, "Ferris - now with extra crab");
      }
      

      Experiments: get the errors on purpose

      Passing the previous tests is the easy part of this chapter. Ownership only really clicks once you've seen the canonical errors with your own eyes, so the messages feel familiar later (the iterators chapter, the ? operator chapter, ...) instead of like a brick wall.

      Each test below is paired with a commented-out line. Uncomment one at a time, run the tests, read the error carefully, then comment it out again before moving on. The errors are the lesson here. The tests themselves don't assert anything interesting.

      The three errors you'll trigger correspond to the three rules of the borrow checker:

      1. You can't use a value after you've moved it.
      2. You can't have two mutable references to the same value at once.
      3. You can't have a mutable reference while a shared reference is still in use.

      Re-read each compiler message until you can explain in one sentence why the compiler is complaining. That's the muscle this chapter is building.

      Useful from the standard library

      • Clone::clone makes an explicit deep copy when you genuinely need two owners. A good "escape hatch" once you've understood why a borrow won't compile, but not the first thing to reach for.
      • The compiler errors themselves are the documentation here. Each one is a paragraph you'd otherwise have to read in a book.
      Exercise 3 of 3
      Open in Web Editor

      Results

        Compiler / runtime output
        
                    
        Reveal the full solution Spoiler: the complete answer
        /// Takes ownership of a String and modifies it.
        fn take_ownership(s: String) -> String {
            let mut s = s;
            s.push_str(" - owned by Rust!");
            s
        }
        
        /// Takes a mutable reference to modify the string in place.
        fn mutate_string(s: &mut String) {
            s.push_str(" - now with extra crab");
        }
        
        #[test]
        fn experiment_use_after_move() {
            let s = String::from("Rust");
            let _result = take_ownership(s);
            // Uncomment the next line. Expect: "borrow of moved value: `s`".
            // assert_eq!(s, "Rust");
        }
        
        #[test]
        fn experiment_two_mutable_borrows() {
            let mut s = String::from("Ferris");
            let r1 = &mut s;
            // Uncomment the next two lines together. Expect:
            // "cannot borrow `s` as mutable more than once at a time".
            // let r2 = &mut s;
            // r2.push('!');
            r1.push('!');
        }
        
        #[test]
        fn experiment_mix_shared_and_mutable() {
            let mut s = String::from("Ferris");
            let shared = &s;
            // Uncomment the next line. Expect:
            // "cannot borrow `s` as mutable because it is also borrowed as immutable".
            // mutate_string(&mut s);
            println!("{shared}");
        }
        

        Wrapping up borrowing

        You borrowed a String read-only as &str, mutated one through &mut String, and triggered the borrow checker's three canonical errors on purpose.

        What we learned

        • A borrow lets you read or modify a value without taking ownership. &T is a shared, read-only borrow; &mut T is an exclusive, writable one.
        • The borrow-checker rule: at any moment, either any number of &T borrows or exactly one &mut T, never both. That's what rules out data races at compile time.
        • Mutability is opt-in at every layer: the binding (let mut x), the parameter (&mut T), and the call site (&mut x).
        • Default to &str over &String (and &[T] over &Vec<T>) for read-only parameters. Slice types accept more callers thanks to deref coercion.
        • The compiler errors are the lesson. Once you can say in one sentence why the compiler is complaining, you've built the muscle this chapter is for.
        Next chapter 7Exercise break: word count