Borrowing and Slices · about 35 min

Dangling References and Why Rust Forbids Them

Why this matters

A dangling reference is a pointer to memory that has already been freed or reused. Following it reads garbage or corrupts someone else's data. In C, returning a pointer to a local variable is one of the most common beginner bugs, and it often seems to work, until it does not. Rust makes dangling references impossible in safe code, with one rule: a reference must never outlive the value it points to. This lesson shows the errors you get when code would break that rule and how to fix each one. It also introduces the word lifetime, which module 10 develops fully.

The bug, in C terms

Imagine a function that builds a label and returns a pointer to it:

fn make_label() -> &String {            main's frame        make_label's frame
    let s = String::from("hi");                             +----------------+
    &s                                                      | s: ptr ----------> [h|i]
}                                                           +----------------+
                                         label ---------->  (the frame above)

after make_label returns:
  - s is dropped: the heap buffer [h|i] is freed
  - make_label's frame is popped; the next function call reuses that stack memory
  - label points at a dead stack slot that pointed at a freed buffer

Two layers of invalid memory. In C this compiles. In Rust it does not.

E0106 and E0515: returning a reference to a local

fn make_label() -> &String {
    let s = String::from("hi");
    &s
}

fn main() {
    let label = make_label();
    println!("{label}");
}
error[E0106]: missing lifetime specifier
 --> main.rs:1:20
  |
1 | fn make_label() -> &String {
  |                    ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`
  |
1 | fn make_label() -> &'static String {
  |                     +++++++
help: instead, you are more likely to want to return an owned value
  |
1 - fn make_label() -> &String {
1 + fn make_label() -> String {
  |

error[E0515]: cannot return reference to local variable `s`
 --> main.rs:3:5
  |
3 |     &s
  |     ^^ returns a reference to data owned by the current function

Two errors. The first, E0106 "missing lifetime specifier", talks about lifetimes, but the key sentence is "this function's return type contains a borrowed value, but there is no value for it to be borrowed from". A returned reference must point at something that existed before the call (passed in by the caller) or that lives for the whole program. A function with no reference parameters has nothing to borrow from. (The 'static suggestion would only be right for string literals and similar; it is not the fix here.)

If you try to satisfy the compiler by writing a lifetime, it catches the real problem anyway:

fn make_label() -> &'static String {
    let s = String::from("hi");
    &s
}

fn main() {
    println!("{}", make_label());
}
error[E0515]: cannot return reference to local variable `s`
 --> main.rs:3:5
  |
3 |     &s
  |     ^^ returns a reference to data owned by the current function

Only the second error, E0515 "cannot return reference to local variable", remains, and it names the real problem. The fix in both cases is to return the owned value:

fn make_label() -> String {
    let s = String::from("hi");
    s
}

fn main() {
    let label = make_label();
    println!("{label}");
}
hi

Ownership moves to the caller; nothing dangles. A good rule of thumb: a function that creates new data returns it owned. Functions return references only when they point into data the caller passed in.

Returning a reference into a parameter: fine

fn first_word(s: &str) -> &str {
    match s.find(' ') {
        Some(i) => &s[..i],
        None => s,
    }
}

fn main() {
    let sentence = String::from("hello brave world");
    let w = first_word(&sentence);
    println!("{w}");
}
hello

The returned &str points into sentence's buffer, which the caller owns and which outlives the call. (s.find(' ') returns the byte position of the first space, or None; &s[..i] is a slice of the first i bytes, covered in the next lesson.) The compiler infers that the result borrows from the parameter, because there is only one parameter it could borrow from. That inference is called lifetime elision.

And the compiler keeps checking at the call site: as long as w is used, sentence may not be modified or dropped.

fn first_word(s: &str) -> &str {
    match s.find(' ') {
        Some(i) => &s[..i],
        None => s,
    }
}

fn main() {
    let mut sentence = String::from("hello brave world");
    let w = first_word(&sentence);
    sentence.clear();
    println!("{w}");
}
error[E0502]: cannot borrow `sentence` as mutable because it is also borrowed as immutable
  --> main.rs:11:5
   |
10 |     let w = first_word(&sentence);
   |                        --------- immutable borrow occurs here
11 |     sentence.clear();
   |     ^^^^^^^^^^^^^^^^ mutable borrow occurs here
12 |     println!("{w}");
   |               --- immutable borrow later used here

clear would empty the buffer w points into. The borrow carried through the function call, so the checker knows w depends on sentence.

E0597: the owner dies first

The same rule applies within one function, when a value goes out of scope before a reference to it is used:

fn main() {
    let r;
    {
        let s = String::from("short-lived");
        r = &s;
    }
    println!("{r}");
}
error[E0597]: `s` does not live long enough
 --> main.rs:5:13
  |
4 |         let s = String::from("short-lived");
  |             - binding `s` declared here
5 |         r = &s;
  |             ^^ borrowed value does not live long enough
6 |     }
  |     - `s` dropped here while still borrowed
7 |     println!("{r}");
  |               --- borrow later used here

E0597 "s does not live long enough", with labels showing where s is dropped and where r is used afterwards. Fixes: make the owner live longer (declare it in the outer scope), or make r an owned value (let r = s; moves the String out of the block instead of borrowing it).

Lifetimes: the idea

All three errors are about the same thing. Every reference has a lifetime: the span of the program during which it is valid to use. Every value has a span during which it exists, from creation to drop. The rule:

   value s exists:      |====================|            (created ... dropped)
   reference r used:         |=========|                   OK: inside s's span
   reference r used:         |=======================|     ERROR: outlives s

Within a function, the compiler works the spans out itself, which is what the previous lessons relied on. At function boundaries, it needs to know how the returned reference relates to the parameters. With exactly one reference parameter, it assumes "the result lives as long as that parameter" (elision). With several, or when storing references inside structs, you will write that relationship down with lifetime annotations like 'a. That is module 10. For now, the practical advice:

Common mistakes

Recap

Going further: read rustc --explain E0106 and rustc --explain E0597. Try writing fn longer(a: &str, b: &str) -> &str and read the error: it is the doorway to module 10.

Exercises

Exercise 1 · write code

Return an owned value

make_label tries to return a reference to a String created inside the function, which would dangle. It fails with E0106. Fix it by returning the String itself (change the return type and the last line). The program must print:

item #7

File: main.rs

Starting point

fn make_label(n: u32) -> &String {
    let s = format!("item #{n}");
    &s
}

fn main() {
    let label = make_label(7);
    println!("{label}");
}

Example

expected output

item #7
Hint 1

s is dropped when make_label returns, so no reference to it can survive.

Hint 2

Return type String, and return s (moving ownership to the caller).

Reference solution
fn make_label(n: u32) -> String {
    let s = format!("item #{n}");
    s
}

fn main() {
    let label = make_label(7);
    println!("{label}");
}

Exercise 2 · write code

Does not live long enough

This program fails with E0597 ("does not live long enough"): r refers to a String that is dropped at the end of an inner block, but r is used after that block. Fix it by moving the String's declaration out of the inner block, so that it lives as long as the reference is used. Keep the inner block (it prints inside). The program must print:

inside
outside: kept

File: main.rs

Starting point

fn main() {
    let r;
    {
        let s = String::from("kept");
        r = &s;
        println!("inside");
    }
    println!("outside: {r}");
}

Example

expected output

inside
outside: kept
Hint 1

Declare s before the inner block, next to let r;.

Reference solution
fn main() {
    let r;
    let s = String::from("kept");
    {
        r = &s;
        println!("inside");
    }
    println!("outside: {r}");
}

Exercise 3 · quiz

Which signatures can work?

Without lifetime annotations, which of these function signatures can be implemented and compile? Select all that apply.

Answer
  • fn first_word(s: &str) -> &str

  • fn make() -> String

A returned reference must point at something that outlives the call. With one reference parameter, the compiler assumes the result borrows from it (lifetime elision), so (a) works. (b) has nothing to borrow from except locals, which die at return (a literal would need the explicit &'static str). (c) returns an owned value: always fine. (d) has two reference parameters, so the compiler cannot guess which one the result borrows from, and asks for an explicit lifetime (module 10).