# Lifetimes

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

Rust roadmap · Core language · 17 min read · https://levelcode.org/roadmaps/rust/lifetimes

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.

> [!IMPORTANT]
> Lifetime annotations don't change how long anything lives. They're **descriptions** of relationships between references that the compiler then checks. You can't make a value live longer by annotating it; you can only tell the compiler how outputs relate to inputs.

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`.

> [!WARNING]
> When the compiler suggests adding `'static` to a reference in a function signature, it's usually the wrong fix. It makes the function accept only literals and leaked data. Look instead for a borrowed value that should be owned, or a lifetime that should be tied to an input.

## Owned or borrowed?

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

| Situation | Usually |
| --- | --- |
| Function parameters you only read | Borrow: `&str`, `&[T]`, `&T` |
| Return values computed inside the function | Owned: `String`, `Vec<T>` |
| Returning a part of an input | Borrow, with a lifetime tying it to that input |
| Struct fields in long-lived state, config, domain models | Owned |
| Short-lived views, parsers, iterators over a buffer | Borrow, with a lifetime parameter on the struct |
| Data shared across threads or tasks | `Arc<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}"));
}
```

**Solution**

1.

```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.

2.

```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`.

3. 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](https://levelcode.org/roadmaps/rust/concurrency).

## 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`.

## Quiz

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

   A. Functions with two parameters always need annotations
   B. &str parameters must be 'static
   C. With two input references, the elision rules can't tell which one the output borrows from
   D. trim's output is owned

   **Answer: C.** Rule 2 only applies with exactly one input lifetime. With two, and no self, the compiler needs you to say how the output relates to the inputs.

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

   A. The returned reference is valid as long as both x and y are
   B. x and y live forever
   C. The function extends y's lifetime to match x's
   D. The returned string is a copy

   **Answer: A.** The single 'a is the overlap of both inputs' lifetimes, so the result can be used only while both are alive. Annotations describe constraints; they don't extend anything.

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

   A. &'a str for a local 'a
   B. String
   C. Parser<'a> holding a borrowed &'a str
   D. &mut Vec<i32> borrowed from a local

   **Answer: B.** T: 'static means T holds no non-static references. An owned String qualifies even though it will be dropped eventually. Anything borrowing a local doesn't.

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?

   A. Make the parser 'static
   B. Clone the parser before each call
   C. Use unsafe to extend the reference
   D. Return Option<&'a str> so tokens borrow from the input, not from self

   **Answer: D.** Without an annotation, elision rule 3 ties the output to &mut self. Naming 'a, the input's lifetime, says the token borrows from the source text, so the parser is free for the next call.
