Step 09 of 17

Lifetimes

Read and write lifetime annotations, apply the elision rules, build structs that borrow, and know when 'static is right.

17 min 8 sections

Every reference in Rust has a lifetime: the stretch of code during which it's valid. Most of the time the compiler works lifetimes out by itself, and you never write them. Lifetime annotations appear when it can't, usually when a function returns a reference and the compiler needs to know which input it borrows from. This lesson explains what annotations mean (and what they don't), the elision rules that let you skip them, structs that hold references, 'static, and how to decide whether to borrow at all.

The problem lifetimes solve

The borrow checker's job is to guarantee no reference outlives the data it points to. Inside one function it can see everything:

rust
fn main() {
    let r;
    {
        let x = 5;
        r = &x;
    } // x is dropped here
    println!("{r}"); // error[E0597]: `x` does not live long enough
}

Across function boundaries, it only looks at signatures, never at other functions' bodies. That keeps compilation modular and errors local. But consider:

rust
fn longest(a: &str, b: &str) -> &str {
    if a.len() >= b.len() { a } else { b }
}
text
error[E0106]: missing lifetime specifier
 --> src/main.rs:1:33
  |
1 | fn longest(a: &str, b: &str) -> &str {
  |               ----     ----     ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `a` or `b`

A caller can't know how long the returned reference stays valid without knowing which argument it came from. The signature has to say.

Lifetime annotations

A lifetime parameter is written 'a (any lowercase name after an apostrophe) and declared in angle brackets like a generic type:

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

Read it as: "for some lifetime 'a, both inputs live at least as long as 'a, and the result is valid for 'a." In practice the compiler picks 'a as the overlap of the two inputs' lifetimes, so the result is valid only while both are.

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

fn main() {
    let outer = String::from("a long string");
    let result;
    {
        let inner = String::from("short");
        result = longest(&outer, &inner);
        println!("{result}"); // fine: both still alive
    }
    // println!("{result}"); // error: `inner` does not live long enough
}

Even though outer is the longer string here, the compiler goes by the signature, which says the result may borrow from either. So result can't be used after inner dies.

If the result only ever comes from one argument, say so, and the other argument is unconstrained:

rust
fn first_word<'a>(text: &'a str, _sep: &str) -> &'a str {
    text.split_whitespace().next().unwrap_or("")
}

Getting the signature right gives callers more freedom: here the separator can be a temporary.

The elision rules

If you had to annotate every reference, Rust would be miserable. Instead, the compiler applies three elision rules to function signatures:

  1. Each reference parameter gets its own lifetime: fn f(a: &str, b: &str) becomes fn f<'a, 'b>(a: &'a str, b: &'b str).
  2. If there's exactly one input lifetime, it's assigned to every output reference: fn trim(s: &str) -> &str means the output borrows from s.
  3. If there's a &self or &mut self parameter, its lifetime is assigned to every output reference: methods returning references are assumed to borrow from self.

If the rules leave an output lifetime undetermined, you get the "missing lifetime specifier" error and must annotate. That's why longest needs one and first_word(s: &str) -> &str doesn't.

Rule 3 is usually what you want, since a getter like fn name(&self) -> &str returns a piece of the struct. When a method returns a reference into an argument instead, annotate it.

Structs that hold references

A struct that holds a reference needs a lifetime parameter, so the compiler can ensure the struct doesn't outlive what it borrows:

rust
struct Parser<'a> {
    input: &'a str,
    pos: usize,
}

impl<'a> Parser<'a> {
    fn new(input: &'a str) -> Self {
        Self { input, pos: 0 }
    }

    /// Returns the next whitespace-separated token, borrowed from the input.
    fn next_token(&mut self) -> Option<&'a str> {
        let rest = &self.input[self.pos..];
        let start = rest.find(|c: char| !c.is_whitespace())?;
        let rest = &rest[start..];
        let len = rest.find(char::is_whitespace).unwrap_or(rest.len());
        self.pos += start + len;
        Some(&rest[..len])
    }
}

fn main() {
    let source = String::from("let x = 42 ;");
    let mut p = Parser::new(&source);
    let mut tokens = Vec::new();
    while let Some(t) = p.next_token() {
        tokens.push(t);
    }
    drop(p);                // the tokens borrow from `source`, not from `p`
    println!("{tokens:?}"); // ["let", "x", "=", "42", ";"]
}

Notice the return type Option<&'a str>, not Option<&str>. With elision rule 3, &str would borrow from &mut self, and every token would keep the parser mutably borrowed, so the loop couldn't call next_token again while holding earlier tokens. Writing 'a says the tokens borrow from the original input, which outlives the parser. This is the most common real-world reason to write a lifetime by hand.

Borrowing structs like this are great for zero-copy parsers, iterators and views over data. For long-lived application state, owned data is almost always simpler.

'static

'static means "valid for the entire program". Two different things use it:

  • &'static T, a reference that's valid forever. String literals are &'static str because they're baked into the binary. So are references to static items, and memory deliberately leaked with Box::leak.
  • T: 'static, a bound meaning "this type contains no non-static references". An owned String, a Vec<u32> or an Arc<Config> all satisfy T: 'static, even though they'll be dropped at some point. It means the value could live forever, because it doesn't borrow anything that might go away.

The second one is what you meet in practice. thread::spawn and tokio::spawn require F: 'static because the new thread or task may outlive the function that created it, so it can't hold references to that function's locals. The fix is almost never to add 'static annotations; it's to move owned data into the closure (move ||), or share it with Arc.

Owned or borrowed?

Lifetimes are a sign you're borrowing. Sometimes the right fix for a lifetime headache is to stop borrowing:

SituationUsually
Function parameters you only readBorrow: &str, &[T], &T
Return values computed inside the functionOwned: String, Vec<T>
Returning a part of an inputBorrow, with a lifetime tying it to that input
Struct fields in long-lived state, config, domain modelsOwned
Short-lived views, parsers, iterators over a bufferBorrow, with a lifetime parameter on the struct
Data shared across threads or tasksArc<T> (shared ownership)

A struct User<'a> { name: &'a str } forces every user of User to carry a lifetime and to keep the source alive. A struct User { name: String } costs an allocation and is far easier to pass around. Start owned; borrow where profiling or API design gives you a reason. Cow<'a, str> ("clone on write") sits in between: it holds either a borrowed or an owned string, useful for functions that usually return their input unchanged but sometimes need to modify it.

Practice

  1. Write fn split_key_value<'a>(line: &'a str) -> Option<(&'a str, &'a str)> that splits "key = value" and returns trimmed slices. Then remove the annotations: does it still compile? Which elision rule applies?
  2. Write a struct Highlights<'a> { text: &'a str } with a method fn sentences_with(&self, word: &str) -> Vec<&'a str> that returns every sentence (split on '.') containing word. Show that the returned sentences can be used after the Highlights value is dropped, and that word can be a temporary.
  3. Explain why this fails and fix it without 'static:
rust
fn spawn_printer(name: &str) {
    std::thread::spawn(|| println!("{name}"));
}
Show solution
rust
fn split_key_value(line: &str) -> Option<(&str, &str)> {
    let (k, v) = line.split_once('=')?;
    Some((k.trim(), v.trim()))
}

It compiles without annotations: there's one input lifetime, so rule 2 assigns it to every output reference.

rust
struct Highlights<'a> {
    text: &'a str,
}

impl<'a> Highlights<'a> {
    fn sentences_with(&self, word: &str) -> Vec<&'a str> {
        self.text
            .split('.')
            .map(str::trim)
            .filter(|s| s.contains(word))
            .collect()
    }
}

fn main() {
    let article = String::from("Rust is fast. Rust is safe. Go is simple.");
    let found = {
        let h = Highlights { text: &article };
        h.sentences_with(&String::from("Rust")) // word is a temporary
    }; // h is dropped here
    println!("{found:?}"); // ["Rust is fast", "Rust is safe"]
}

The return type names 'a, the lifetime of text, so the sentences borrow from article, not from h or word. With an elided Vec<&str>, rule 3 would tie the result to &self, and found couldn't outlive h.

  1. The spawned thread might run after spawn_printer returns, when name (borrowed from the caller) may be gone. thread::spawn requires a 'static closure. Move an owned copy in:
rust
fn spawn_printer(name: &str) -> std::thread::JoinHandle<()> {
    let name = name.to_string();
    std::thread::spawn(move || println!("{name}"))
}

Returning the JoinHandle lets the caller wait for it. If the thread only needs to live within the caller's scope, std::thread::scope allows borrowing directly; see the concurrency lesson.

Key takeaways

  • A lifetime is the span during which a reference is valid; annotations describe how references relate, they never extend how long anything lives.
  • The compiler checks across functions using signatures only, so returned references must say which input they borrow from.
  • Three elision rules cover most signatures: one input lifetime, or &self, flows to the outputs.
  • Structs holding references need a lifetime parameter; return &'a T from their methods when results borrow from the source, not from self.
  • T: 'static means "owns everything it holds". Satisfy it by moving owned data or using Arc, not by annotating references 'static.

Check yourself

Quiz

0/4 correct · 4 left

  1. 1.Why does fn pick(a: &str, b: &str) -> &str need a lifetime annotation while fn trim(s: &str) -> &str doesn't?

  2. 2.What does fn longest<'a>(x: &'a str, y: &'a str) -> &'a str promise?

  3. 3.Which of these types satisfies the bound T: 'static?

  4. 4.A method fn next_token(&mut self) -> Option<&str> on Parser<'a> keeps the parser mutably borrowed while you hold a token. What's the fix?

Concepts in this lesson

One-page notes on each idea, for review or a quick lookup later.

Finished reading?

Mark it complete to fill in your roadmap.