Level 3 · Power Tools

Lifetimes ⏳

The infamous 'a. Rust's most feared syntax is actually one honest question, asked politely: "how long does this borrow live?" Let's take the fangs out.

The crime lifetimes prevent

In C, this is a legendary way to ruin a weekend: keep a pointer to something, free the thing, use the pointer anyway. A dangling reference: reading memory that now belongs to someone else. Here's the Rust equivalent:

fn main() {
    let borrowed;
    {
        let temp = String::from("short-lived");
        borrowed = &temp;         // borrow temp...
    }                             // ...but temp dies HERE
    println!("{borrowed}");       // ❌ using a reference to a corpse
}
error[E0597]: `temp` does not live long enough
  |
4 |         borrowed = &temp;
  |                    ^^^^^ borrowed value does not live long enough
5 |     }
  |     - `temp` dropped here while still borrowed
6 |     println!("{borrowed}");
  |               ---------- borrow later used here

Rust catches it because it tracks a lifetime for every reference: the region of code where the borrowed data is guaranteed alive. Rule (you know it from Lesson 7): a reference must never outlive what it points to. Usually this checking is invisible. Sometimes the compiler needs a hint; that's the only time you see 'a.

When the compiler asks for help

Here's the classic function that needs a hint, returning the longer of two strings:

fn longest(a: &str, b: &str) -> &str {   // ❌ won't compile
    if a.len() > b.len() { a } else { b }
}

The compiler's dilemma: the returned reference points at a's data or b's data; it can't know which until runtime. So how long is the result safe to use? It refuses to guess. We answer with a lifetime annotation:

fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() > b.len() { a } else { b }
}

Read 'a as a label, like "lifetime A." The signature now says: "both inputs live at least as long as some region 'a, and my result is only valid inside that same region." In practice: the result is safe as long as the shorter-lived of the two inputs. That's all a lifetime annotation is: paperwork connecting outputs to inputs. It changes nothing at runtime; it exists only so the compiler can check callers:

fn main() {
    let long_lived = String::from("barnacle");

    let result;
    {
        let short_lived = String::from("kelp");
        result = longest(&long_lived, &short_lived);
        println!("{result}");    // ✅ fine: both inputs still alive
    }
    // println!("{result}");     // ❌ short_lived is gone; result might
                                 //    point at it, so the compiler says no
}

The library card analogy 📇

A reference is a library card for a book you don't own. A lifetime is the card's expiry date: the compiler stamps it to match when the book leaves the library. longest<'a> just tells the stamp machine: "this card's expiry = the earlier of the two books' due dates." Expired card? The librarian (borrow checker) stops you at the door. Nobody ever reads a book that isn't there.

Why you'll rarely write these

Good news: in common patterns, the compiler stamps cards automatically; that's called lifetime elision. This needs no annotation:

fn first_word(text: &str) -> &str {
    text.split_whitespace().next().unwrap_or("")
}

One input reference → the output obviously borrows from it → the compiler fills in the lifetimes silently. You only write 'a when there's genuine ambiguity (like two inputs). Most Rust code has almost no visible lifetimes.

One special lifetime you've met already: 'static means "alive for the whole program". That's true of string literals, which are baked into the binary: let motto: &'static str = "stay rusty";

Structs that borrow

If a struct holds a reference, it needs the annotation too: "this struct must not outlive the data it's viewing":

struct Excerpt<'a> {
    line: &'a str,
}

fn main() {
    let poem = String::from("the tide returns. the crab abides.");
    let quote = Excerpt { line: &poem[0..17] };
    println!("{}", quote.line);
}   // poem and quote die together, all good
💡 Beginner escape hatch Fighting a lifetime error you don't understand? Store an owned String instead of a &str, or return String instead of &str. Owning more costs a little memory and buys a lot of peace. Borrow-everywhere is an optimization, not a moral duty.
⚠️ Common stumbles
  • Returning a reference to a variable created inside the function: it dies at the closing brace; nothing can save it. Return an owned value.
  • Reading 'a as something mystical: it's a label, like naming a generic T.
  • Adding lifetimes everywhere preemptively: write without them, add only when the compiler asks.
Exercise 1

Feel the error

Type the "short-lived" example from the top of this lesson into the Playground and read the full error. Then fix it by moving the println! inside the inner braces. Why does that satisfy the checker?

Reveal answer

Inside the braces, temp is still alive, so the reference is valid at the moment it's used. The borrow now ends before its target dies: rule respected, everyone happy.

Exercise 2

Shortest, not longest

Write fn shortest<'a>(a: &'a str, b: &'a str) -> &'a str returning the shorter string, and prove it works. (Mostly muscle memory; that's the point.)

Reveal solution
fn shortest<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() <= b.len() { a } else { b }
}

fn main() {
    println!("{}", shortest("barnacle", "kelp"));  // kelp
}
Exercise 3

The impossible function

Explain why this can never compile, no matter what lifetime annotations you add. Then write the version that works.

fn make_greeting() -> &str {
    let text = String::from("ahoy!");
    &text
}
Reveal answer

text is dropped when the function returns; the reference would point at freed memory, and no annotation can extend a dead value's life. Lifetimes describe reality; they don't change it. The fix is to hand over ownership instead:

fn make_greeting() -> String {
    String::from("ahoy!")
}