Chapter 16

A Primer on Lifetimes

Did you know that they make a movie about Rust?
It's called "The Story of 'a lifetime".

You've already used ownership in several forms. You moved Strings, borrowed slices, passed &mut references into functions, and handed values to Vec, HashMap, Option, Result, and your own structs. Now we can connect those operations and see how Rust manages memory safely without a garbage collector.

What the Borrow Checker Checks

Languages with manual memory management (C, C++) hand you the power to free memory yourself, and with it the power to free it twice, free it too early, or forget to free it at all. Languages with a garbage collector take that power back and spend runtime and memory tracking what's still alive. Rust takes a third path: before the program runs, the compiler checks where owned values are dropped and whether references outlive what they point at.

Three rules summarize the model you've already been using:

  1. Every value has exactly one owner.
  2. When the owner goes out of scope, the value is dropped.
  3. You can borrow a value without owning it, under the aliasing rule (many &, or one &mut, never both).

Rules 1 and 2 give each value one place where cleanup normally happens, which rules out double-frees in safe Rust. The aliasing part of rule 3 rules out data races because you can't write through one reference while reading through another. Lifetime checks supply the other half: a reference can't remain usable after its owner has been dropped.

For an owned String, dropping the value frees its buffer automatically. If a reference would still be used after that drop, safe Rust rejects the code at compile time.

A First Look at Lifetimes

The borrow checker also tracks how long a reference stays valid. That's what a lifetime describes.

A reference borrows a value, so it must not outlive that value. If it could, you'd have a reference pointing at memory that's already been cleaned up, which is a use-after-free bug.

Picture a function that tries to return a reference to its own local string:

fn dangling() -> &String {
    let s = String::from("temporary");
    &s   // ERROR: `s` is dropped at the end of this function
}

s is owned by dangling, so it's dropped the moment the function returns. A reference to it would dangle, so the compiler rejects the code. When reading this error, I'd start by asking who owns s and when it gets dropped. A borrow has to stay valid for as long as you use it, and the compiler checks that span (the reference's lifetime) against the owner's.

Lifetimes can have a name. By convention, you name them 'a, 'b, and so on, though more descriptive names are allowed too.

fn dangling<'a>() -> &'a String {
    let s = String::from("temporary");
    &s   // Still an error: naming a lifetime doesn't keep `s` alive
}

In this example, <'a> declares a lifetime parameter, and &'a String uses it for the returned reference.

This is the same attempt to return a reference to a local value, and it fails for the same ownership reason. Strictly speaking, the first signature also has a missing lifetime: there is no input reference whose lifetime the compiler can use for the return value. Writing 'a addresses that missing annotation, but it doesn't extend the life of s. To return this string, return an owned String instead of a &String reference.

Most of the time the compiler works lifetimes out on its own and you never write one. You'll usually need annotations when a struct holds a reference or a function returns one of several borrowed inputs. The syntax can take some getting used to, but keep the same question in mind: which owner does this reference depend on, and will that owner still be alive?

When the compiler reports a lifetime error, trace the borrowed value, its owner, and the last use of the reference.

Keep a Username

In the Option<T> chapter, find_user_by_id returned a borrowed username. Now the caller needs to keep that name after the user records are dropped.

Run the supplied code first: three tests pass, and test_username_survives_records stops at todo!().

  1. Read the commented code in test_username_survives_records. Before uncommenting it, predict whether it will compile. Which value owns the text, and when is that value dropped?
  2. Remove the todo!() and the /* and */, then run the tests. Read the compiler error before editing the function.
  3. Change the return type and body of find_user_by_id so the caller can use the username after the records are dropped. Keep the borrowed input parameter and all tests unchanged. The records must still be dropped at the end of their block.

The lookup must still return the first matching username, or None for a missing ID. The caller must still be able to use its records after the call. You don't need lifetime annotations for this task.

All four tests should pass when you're done. To set the exercise aside, restore the last test's comment markers and todo!() rather than removing the test.

When is the borrowed return from the Option<T> chapter a better choice? What extra work does your implementation do to keep a separate username?

Exercise
Open in Web Editor

Results

    Compiler / runtime output
    
                
    Reveal the full solution Spoiler: the complete answer
    /// Finds the first user with this ID, or returns `None`.
    fn find_user_by_id(users: &[(u32, String)], id: u32) -> Option<String> {
        users
            .iter()
            .find(|(uid, _)| *uid == id)
            .map(|(_, name)| name.clone())
    }
    
    // Cloning the matching username leaves the caller's records unchanged. The
    // returned String has its own buffer, so dropping the records won't free it.
    // Returning &str avoids the copy if the records stay alive while we use the
    // name.
    
    #[test]
    fn test_find_user_by_id() {
        let mut users = vec![(1, String::from("alice")), (2, String::from("bob"))];
        assert_eq!(find_user_by_id(&users, 2).as_deref(), Some("bob"));
        assert_eq!(find_user_by_id(&users, 99).as_deref(), None);
        users.push((3, String::from("charlie")));
        assert_eq!(find_user_by_id(&users, 3).as_deref(), Some("charlie"));
    }
    
    #[test]
    fn test_empty_records() {
        assert_eq!(find_user_by_id(&[], 1).as_deref(), None);
    }
    
    #[test]
    fn test_first_match() {
        let users = [
            (7, String::from("Férris 🦀")),
            (7, String::from("impostor")),
        ];
        assert_eq!(find_user_by_id(&users, 7).as_deref(), Some("Férris 🦀"));
    }
    
    #[test]
    fn test_username_survives_records() {
        let username = {
            let users = [(42, String::from("Férris 🦀"))];
            find_user_by_id(&users, 42)
        };
        assert_eq!(username.as_deref(), Some("Férris 🦀"));
    }
    

    Wrapping Up

    Ownership is the model underneath the moves, borrows, and references you've been using throughout the course. In the username exercise, returning an owned String let the caller keep the name after the records were dropped.

    The Whole Picture

    Cleanup and Memory Safety

    C++ uses RAII and destructors for deterministic cleanup, while garbage-collected languages track which values are still alive at runtime. Rust adds compile-time ownership and borrowing rules, so safe Rust turns use-after-free, double-free, and data races into compile errors.

    Next chapter 17Traits

    Optional Chapters

    Extra practice to explore at your own pace. These chapters do not count toward course progress.

    ← Return to the Rust course curriculum