Frontend Labs
⋯

19. Lifetimes: naming how long a borrow is valid

19.1 What a lifetime is

A lifetime is not how long a value lives. It is a name for the region of code in which a borrow must stay valid. Most of the time the compiler infers them. You write them only when a function or struct relates the lifetimes of several references and the compiler cannot work out which input the output borrows from.

// DOES NOT COMPILE
fn longer(a: &str, b: &str) -> &str {
    if a.len() >= b.len() { a } else { b }
}
fn main() { println!("{}", longer("x", "yy")); }
error[E0106]: missing lifetime specifier
  |     fn longer(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`

The fix states the relationship: "the result borrows from both inputs, so it is valid only while both are":

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

fn main() {
    let outer = String::from("long string");
    let result;
    {
        let inner = String::from("xyz");
        result = longer(&outer, &inner);
        println!("{result}"); // fine: `inner` is still alive here
    }
    // println!("{result}"); // E0597: `inner` does not live long enough
}

Lifetimes are checked, never chosen. Annotations do not make anything live longer. They describe constraints, and the compiler checks that every call site satisfies them.

19.2 The three elision rules

You rarely write lifetimes because the compiler fills them in using three rules:

  1. Each reference parameter gets its own lifetime.
  2. If there is exactly one input lifetime, the output gets that lifetime.
  3. If one of the parameters is &self or &mut self, the output gets self's lifetime.

So fn first_word(s: &str) -> &str and fn name(&self) -> &str need no annotations. fn longer(a: &str, b: &str) -> &str has two inputs and no self, so none of the rules decide the output and you must write it.

19.3 Structs that borrow: zero-copy parsing

#[derive(Debug)]
struct RequestLine<'a> {   // this struct cannot outlive the buffer it borrows from
    method: &'a str,
    path: &'a str,
    version: &'a str,
}

fn parse_request_line(line: &str) -> Option<RequestLine<'_>> { // '_ = "the elided lifetime"
    let mut parts = line.split(' ');
    let (method, path, version) = (parts.next()?, parts.next()?, parts.next()?);
    if parts.next().is_some() || !version.starts_with("HTTP/") { return None; }
    Some(RequestLine { method, path, version })
}

fn main() {
    let raw = String::from("GET /index.html HTTP/1.1");
    let req = parse_request_line(&raw).expect("valid request line");
    println!("{req:?}");   // no allocation: every field points into `raw`
    // drop(raw);          // E0505: cannot move out of `raw` because it is borrowed
    println!("{}", req.path);
}

This is how fast parsers such as httparse (used by Hyper, Ch 50), serde borrowed deserialization, and nom work. They allocate nothing, and the compiler makes sure the parsed view cannot outlive the buffer it points into. In C, the same design is a common source of use-after-free bugs.

19.4 'static: two meanings you must keep apart

  • &'static T: a reference valid for the whole program, such as string literals, static items, and leaked boxes.
  • T: 'static (a bound): "T contains no non-static borrows". Owned types such as String and Vec<u8> satisfy it. It does not mean "lives forever". thread::spawn and tokio::spawn require T: 'static because the new thread or task may outlive the current stack frame, so it must own everything it uses (Ch 28).
fn needs_static<T: 'static + std::fmt::Debug>(t: T) { println!("{t:?}"); }

fn main() {
    needs_static(String::from("owned")); // OK: String owns its data
    needs_static(42);                    // OK
    let local = String::from("x");
    // needs_static(&local);             // E0597: `local` does not live long enough
    needs_static(local.clone());         // the usual fix: give it an owned value
}

19.5 When you are fighting lifetimes, own the data instead

If a struct with lifetime parameters spreads 'a through half your codebase, store owned data (String, Vec<u8>, Arc<str>) instead. Use borrowing structs for short-lived views such as parsers and iterators. Use owned structs for anything that gets stored, sent to another thread, or returned from an API. The extra allocation is usually cheaper than the design cost.


Generated from real-life-rust-guide.md · 8 Oct 2026 16:36