Updated Aug 6, 2026

15. Lifetimes

📋 Jump to Takeaways

🎁 What if you returned a reference to a variable that goes out of scope the moment the function ends? In C, that's a silent use-after-free bug. In Rust, the compiler refuses to build it, and it does so by tracking something called a lifetime.

Why Lifetimes Exist

Lifetimes prevent dangling references, pointers to memory that has already been freed. In C or C++, this is a use-after-free bug that leads to crashes or security vulnerabilities. Rust catches it at compile time.

fn main() {
    let r;
    {
        let x = 5;
        r = &x;
    } // x is dropped here
    // println!("{}", r); // ❌ ERROR: `x` does not live long enough
}

The compiler sees that x lives only inside the inner block, but r tries to use it outside. Rust refuses to compile this. No runtime check, just static analysis using lifetimes.

Lifetime Annotations on Functions

When a function takes multiple references and returns a reference, the compiler needs help. It can't tell which input the output borrows from.

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

fn main() {
    let s1 = String::from("long string");
    let result;
    {
        let s2 = String::from("hi");
        result = longest(s1.as_str(), s2.as_str());
        println!("{}", result); // Works here
    }
    // println!("{}", result); // ❌ ERROR: result's lifetime is tied to s2, which is dropped here
}

The annotation 'a says: "the returned reference lives at least as long as the shorter of x and y." You're not changing how long things live, you're telling the compiler how the lifetimes relate.

The compiler doesn't run the if to see which branch actually executes, it only looks at the signature and assumes the result could be either x or y. That's why result's lifetime gets tied to s2 (the shorter-lived input) even though "long string" is the one actually returned at runtime.

That final commented-out line isn't 'a failing to help, it's 'a doing its job. 'a is what lets the compiler prove the first println! is safe and catch that the second one isn't. In a language without lifetime checking, that second line would compile and run, reading memory that's already been freed. Rust turns that into a compile-time error instead.

Note that 'a isn't a generic type like T. A generic type parameter says "this can be any type." A lifetime parameter says "this reference is valid for at least this span of code," and it lets you tell the compiler how the lifetimes of two or more references relate to each other. The apostrophe is what marks it as a lifetime instead of a type; the name 'a itself is just convention, like T is for generics.

You can mix both in the same signature, they're independent parameters that just happen to share the angle brackets:

use std::fmt::Display;

fn longest_with_announcement<'a, T: Display>(x: &'a str, y: &'a str, ann: T) -> &'a str {
    println!("Announcement: {}", ann);
    if x.len() > y.len() { x } else { y }
}

fn main() {
    let result = longest_with_announcement("long string", "hi", "comparing lengths");
    println!("{}", result); // "long string"
}

'a still governs how long x and y (and the returned reference) are valid for. T has nothing to do with lifetimes, it just says "ann can be any type that implements Display." Changing T to a different type doesn't affect 'a, and changing 'a doesn't affect what T can be.

When the Compiler Can't Figure It Out

With one reference parameter, the compiler knows the output must borrow from it, no annotation needed:

// One reference parameter — no ambiguity, no 'a needed
fn first_letter(s: &str) -> &str {
    &s[..1]
}

fn main() {
    let letter = first_letter("hello");
    println!("{}", letter); // "h"
}

But with two or more reference parameters, ambiguity appears.

// This won't compile — which input does the return borrow from?
// fn first_word(s1: &str, s2: &str) -> &str {
//     &s1[..1]
// }

// Fix: annotate to show it borrows from s1
fn first_word<'a>(s1: &'a str, _s2: &str) -> &'a str {
    &s1[..1]
}

fn main() {
    let word = first_word("hello", "world");
    println!("{}", word); // "h"
}

Notice _s2 doesn't need 'a because the return value doesn't borrow from it. Prefixing with _ tells the compiler you intentionally aren't using this parameter. You only annotate the relationships that matter.

Lifetime Elision Rules

You don't always write lifetime annotations. The compiler applies three rules automatically:

Rule 1: Each reference parameter gets its own lifetime. fn foo(x: &str, y: &str) becomes fn foo<'a, 'b>(x: &'a str, y: &'b str).

// You write:
fn compare(x: &str, y: &str) -> bool { x.len() > y.len() }

// Compiler sees (Rule 1 — each reference gets its own lifetime):
fn compare<'a, 'b>(x: &'a str, y: &'b str) -> bool { x.len() > y.len() }

The two parameters get independent lifetimes 'a and 'b — they don't have to come from the same source. No output reference here, so Rule 1 is all that's needed.

Rule 2: If there's exactly one input lifetime, it's assigned to all output lifetimes. fn foo(x: &str) -> &str becomes fn foo<'a>(x: &'a str) -> &'a str.

Rule 3: If one of the parameters is &self or &mut self, the lifetime of self is assigned to all output lifetimes.

// No annotations needed — Rule 2 applies
fn first_three(s: &str) -> &str {
    &s[..3]
}

// No annotations needed — Rule 3 applies
struct Config {
    name: String,
}

impl Config {
    fn get_name(&self) -> &str {
        &self.name
    }
}

fn main() {
    let name = first_three("hello");
    println!("{}", name); // "hel"
}

If the three rules don't fully determine output lifetimes, the compiler asks you to annotate explicitly.

Lifetimes in Structs

When a struct holds a reference, you must annotate it. This tells the compiler: "this struct cannot outlive the data it borrows."

#[derive(Debug)]
struct Excerpt<'a> {
    text: &'a str,
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().unwrap(); // .split() returns an iterator; .next() grabs the first item
    
    let excerpt = Excerpt { text: first_sentence };
    println!("{:?}", excerpt);
    // Excerpt { text: "Call me Ishmael" }
}

If you tried to use excerpt after novel is dropped, the compiler would reject it. The lifetime 'a on the struct enforces this. You'll see the fix for this pattern in "Common Lifetime Errors" below.

The 'static Lifetime

The 'static lifetime means the reference lives for the entire program. String literals are 'static because they're baked into the binary.

fn get_greeting() -> &'static str {
    "Hello, world!" // string literals are 'static
}

fn main() {
    let s: &'static str = "I live forever";
    println!("{}", s);              // I live forever
    println!("{}", get_greeting()); // Hello, world!
}

You'll also see 'static in trait bounds like T: Send + 'static (you'll meet Send in the Concurrency lesson), meaning the type owns all its data (no borrowed references). Don't slap 'static on everything to "fix" lifetime errors, it usually means you should restructure your code instead.

Common Lifetime Errors and Fixes

Error: returning a reference to a local variable.

// BROKEN
// fn make_greeting(name: &str) -> &str {
//     let greeting = format!("Hello, {}", name);
//     &greeting // ❌ ERROR: returns reference to local
// }

// FIX: return an owned String instead
fn make_greeting(name: &str) -> String {
    format!("Hello, {}", name)
}

fn main() {
    let g = make_greeting("Rust");
    println!("{}", g); // "Hello, Rust"
}

Error: conflicting lifetimes.

// BROKEN — compiler can't satisfy both lifetimes
// fn pick<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
//     if x.len() > 0 { x } else { y } // ❌ ERROR: y has lifetime 'b, not 'a
// }

// FIX: use the same lifetime for both
fn pick<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > 0 { x } else { y }
}

fn main() {
    let result = pick("hello", "world");
    println!("{}", result); // "hello"
}

When both parameters share 'a, the compiler uses the shorter actual lifetime of the two, same rule as longest above.

Error: struct outlives borrowed data.

// BROKEN
// fn create_excerpt() -> Excerpt {
//     let data = String::from("temp");
//     Excerpt { text: &data } // ❌ ERROR: data dropped at end of fn
// }

// FIX: ensure the data lives long enough, or store owned data
struct OwnedExcerpt {
    text: String,
}

fn create_excerpt() -> OwnedExcerpt {
    let data = String::from("temp");
    OwnedExcerpt { text: data }
}

The pattern is clear: if you can't guarantee the borrowed data outlives the reference, switch to owned data.

Key Takeaways

  • Lifetimes prevent dangling references at compile time, no runtime cost
  • Annotations like 'a describe relationships between references, they don't change how long data lives
  • The compiler applies three elision rules so you rarely write annotations in practice
  • Structs holding references need lifetime annotations
  • 'static means "lives for the entire program", use it sparingly
  • When lifetime errors appear, consider whether you should return owned data instead

🎁 References borrow data without owning it. But what if you want a single value that multiple parts of your program can own — and it cleans itself up when the last owner is done? Next up: smart pointers, the types that make that possible.

📝 Ready to test your knowledge?

Answer the quiz below to mark this lesson complete.

Spot something off? Report an issue
© 2026 ByteLearn.dev. Free courses for developers. · Privacy