Weekend vibes. You’re ready to learn Rust. You write a function to return a string slice, and the compiler smacks you with:

error[E0106]: missing lifetime specifier
  --> src/main.rs:2:33
   |
2  | fn longest(x: &str, y: &str) -> &str {
   |               ----     ----     ^ expected named lifetime parameter

What the hell? Lifetime specifier? I just wanna return a string and now you want me to annotate lifetimes? You open Stack Overflow and see a bunch of 'a, 'b, 'static—suddenly feels like high school math all over again. Brain goes: what is this, why do I need this, can’t we keep it simple? You close the IDE, go back to Python scripting. Rust’s too hard, maybe next time.

But wait. What if I told you Rust lifetimes are just library borrowing rules? Would you still think they’re terrifying?

Imagine you’re a librarian. Two readers, A and B, each borrowed a book. A new reader C asks: “Which book is thicker? I want to borrow that one.” What would you do? You’d check A and B’s library cards to see when they’re returning the books, pick the one with the later return date, and ensure C returns it before A or B do. That’s the essence of Rust lifetimes—lifetime annotations tell the compiler which data this reference is borrowing and until when.

The Rust compiler is like a librarian. It ensures the book you borrowed (the data you reference) won’t be returned (freed) before you finish using it, and you can’t lend it to someone else and return it yourself first (dangling reference). Back to the example—you wrote this function:

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

The compiler’s confused. Are you returning x or y? Their borrowing periods (lifetimes) might be different. It’s like the librarian asking: “A’s book is due next week, B’s is due tomorrow. Which one are you lending to C? How do I know when C should return it?” So you gotta explicitly tell the compiler the return value’s lifetime relates to the input parameters’ lifetimes.

Correct version:

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

In plain English: this function receives two references with lifetime 'a, and returns a reference also with 'a—meaning the return won’t outlive the inputs. Like telling the librarian: C’s return date is at most the earlier return date between A and B.

Three Common Scenarios

Say you’re making a book excerpt card with the title and a quote:

struct BookExcerpt {
    title: &str,      // Error! Missing lifetime annotation
    content: &str,
}

The compiler yells again: “Where’d you copy this title and content from? If that book gets returned (data freed), your card becomes useless, right?” Correct version:

struct BookExcerpt<'a> {
    title: &'a str,
    content: &'a str,
}

Meaning this card’s lifetime can’t exceed the book it references. Like your excerpt card is only valid while the original book is still in the library. Once the book is returned, the card is meaningless.

Returning references from functions is similar:

fn first_word<'a>(s: &'a str) -> &'a str {
    s.split_whitespace().next().unwrap()
}

This function takes a string reference and returns a reference to the first word. The 'a lifetime tells the compiler the return comes from the input—they have the same lifetime. Like photocopying a page from a book—the photocopy’s validity doesn’t outlive the original book.

There’s also a special one called static lifetime 'static, meaning the data is valid for the entire program runtime:

let s: &'static str = "Hello, world!";

String literals are 'static because they’re hardcoded into the binary—they won’t disappear unless the program shuts down. Like the library’s treasure collection—never gets lent out, always available for reference.

Actually, most of the time you don’t need to write lifetime annotations manually. The Rust compiler has three lifetime elision rules: each reference parameter gets its own lifetime, if there’s only one input reference the output automatically uses its lifetime, and in methods with &self or &mut self the return automatically uses self’s lifetime. Like an experienced librarian—they can figure out the borrowing rules themselves most of the time without you explaining every detail.

Here’s the key: lifetime annotations don’t change the actual lifetime of data. They just tell the compiler “I know how long this reference lives” so the compiler can check for violations. Like the due date on a library card doesn’t make the book live longer or shorter—it just lets the librarian know when to remind you to return it.

Code Examples

Simplest reference:

fn main() {
    let s = String::from("hello");
    let r = &s;  // r borrows s
    println!("{}", r);
}  // s and r go out of scope together, no problem

Like borrowing a book, using it, and returning it with the book—no issues.

Dangling reference (won’t compile):

fn main() {
    let r;
    {
        let s = String::from("hello");
        r = &s;  // Error! s is about to be freed
    }  // s gets dropped here
    println!("{}", r);  // r references data that's gone
}

Like borrowing a book but it gets returned to the library before you finish using it—you’re left with just a title card. The compiler yells:

error[E0597]: `s` does not live long enough

Lifetimes in structs:

#[derive(Debug)]
struct BookExcerpt<'a> {
    title: &'a str,
    content: &'a str,
}

fn main() {
    let book = String::from("The Rust Programming Language");
    let excerpt = BookExcerpt {
        title: &book,
        content: "Lifetimes are simple",
    };

    println!("{:?}", excerpt);
}  // book and excerpt drop together

Success! Because excerpt’s referenced book is valid throughout the entire scope.

Returning the longer-lived string:

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("short");
        result = longest(&s1, &s2);
        println!("{}", result);  // Correct: result used here
    }
    // println!("{}", result);  // Error: s2 already freed
}

longest returns a reference with the shorter lifetime between s1 and s2, so result can’t be used after s2 is freed.

Common Pitfalls

Lifetime annotations don’t make data live longer—they just tell the compiler relationships:

fn bad_idea<'a>() -> &'a str {
    let s = String::from("hello");
    &s  // Error! s gets freed at function end
}

Like trying to label a one-day book as “permanent loan”—the librarian won’t allow it.

Most of the time you don’t need that many lifetime parameters. If multiple references have the same lifetime, one 'a is enough. Don’t write stuff like this:

struct Complex<'a, 'b, 'c> {
    x: &'a str,
    y: &'b str,
    z: &'c str,
}

When the compiler errors, don’t rush to slap 'static everywhere or use clone() to bypass checks. First think: what’s my data borrowing relationship actually like? Most of the time the compiler’s right—it’s protecting you from writing code that’ll crash.

And don’t forget the lifetime elision rules—the compiler will auto-infer, you don’t need to write it every time. Writing fn get_part(s: &str) -> &str is cleaner than fn get_part<'a>(s: &'a str) -> &'a str.

Lifetimes are borrowing rules—who borrowed the data, until when, and the compiler checks for violations. Most of the time you don’t write them, the compiler auto-infers. Only when it can’t figure it out do you need to annotate. Listen to the compiler—it yells at you for your own good. Don’t rush to bypass checks, first understand what it’s saying.

Here’s a cheat sheet:

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

// Lifetimes in structs
struct BookExcerpt<'a> {
    title: &'a str,
    content: &'a str,
}

// Static lifetime
let s: &'static str = "Hello, world!";

// Lifetime elision (compiler auto-infers)
fn get_part(s: &str) -> &str {
    &s[0..5]
}

Remember this: lifetimes don’t make data live longer—they ensure the compiler confirms you won’t use data that’s already dead.

How did you solve your first lifetime error? Did you also see all those 'a, 'b and freak out at first, then realize “oh, it’s just borrowing rules”? Or do you have a better mental model? Drop your war stories in the comments.


Found this useful? If this article helped you understand Rust lifetimes or stopped you from running away from compiler errors, hit that like button so more folks who got scared off can see it, share it with your colleagues—especially those “Rust is too hard, I’ll just learn Go” friends, follow Dream Beast Programming for upcoming talks on ownership, smart pointers, and other scary-but-not-really topics, and drop a comment sharing your first lifetime error story.

Your support keeps me writing.