Skip to content

Lifetimes

A lifetime annotation like 'a tells the compiler how references relate to each other; it never makes a value live longer.

In Java, a reference keeps its object alive. If a method returns one of its arguments, the caller can keep the result for as long as it wants. The garbage collector does not free the object while any reference to it exists.

Java

static String longer(String first, String second) {
return second.length() > first.length() ? second : first;
}
static String demo() {
String name;
{
String nickname = "Ada";
name = longer("Ada Lovelace", nickname);
} // `nickname` is out of scope, but its object lives on
return name;
}

Rust

pub fn longer<'a>(first: &'a str, second: &'a str) -> &'a str {
if second.len() > first.len() {
second
} else {
first
}
}

Rust has no garbage collector, so a reference does not keep its value alive. The value is freed when its owner goes out of scope (see Ownership and moves). The compiler must prove that no reference is used after that point. Lifetimes are how it does this.

A lifetime is a name for “the region of code where this reference is valid”. It starts with an apostrophe: 'a, 'b, 'de, 'static. Most code has no lifetime annotations at all. You see them where a function returns a reference, and the compiler cannot guess which input it comes from:

Does not compile: E0106
fn longer(first: &str, second: &str) -> &str { // error E0106: missing lifetime specifier
if second.len() > first.len() { second } else { first }
}

Two references go in, and one comes out. Is the result valid as long as first, or as long as second? The compiler does not look at the body to decide. It looks only at the signature, and the signature does not say. With the annotation, it does:

pub fn longer<'a>(first: &'a str, second: &'a str) -> &'a str {
if second.len() > first.len() {
second
} else {
first
}
}
  1. Line 1: <'a> declares a lifetime parameter, like a generic type parameter. &'a str is "a string slice that is valid for at least 'a". The result has the same 'a: it is valid only while **both** inputs are valid.
  2. Line 2-6: The body is normal code. Lifetimes change nothing at run time. They exist only for the compiler.

Read fn longer<'a>(first: &'a str, second: &'a str) -> &'a str as: “the result borrows from first or second, so do not use it after either of them is gone”. The compiler uses this to reject code like the following, where nickname is freed while name can still point to it:

Does not compile: E0597
let customer = String::from("Ada Lovelace");
let name;
{
let nickname = String::from("Ada");
name = longer(&customer, &nickname); // error E0597: `nickname` does not live long enough
} // `nickname` is dropped here, but `name` may still point to it
println!("{name}");

In Java this is fine, because name keeps the string alive. In Rust, the fix is to change the scopes, or to return an owned String instead of a reference.

Lifetime elision: why most code has no annotations

Section titled “Lifetime elision: why most code has no annotations”

The compiler fills in lifetimes for common cases. This is called lifetime elision. The rules, simplified:

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

So fn first_word(text: &str) -> &str needs no annotation: there is one input reference, so the result borrows from it. A method fn name(&self) -> &str needs none either: the result borrows from self. You need annotations only when the rules do not give an answer, as in longer.

'static is the lifetime of the whole program. You see it in two places, and they mean different things:

  • &'static str: a reference that is valid until the program ends. String literals have this type, because they are part of the program’s binary.
  • T: 'static as a bound: the type T contains no references that could become invalid. An owned String or Vec<u64> is 'static in this sense. It does not mean “lives forever”. It means “does not borrow anything short-lived”.
pub const DEFAULT_CURRENCY: &str = "EUR"; // a literal is `&'static str`

thread::spawn requires F: 'static, because the new thread can run longer than the function that started it. So the closure must own its data, not borrow it. That is what move does:

/// `thread::spawn` needs `F: 'static`: the closure must not borrow
/// anything from the caller's stack, because the thread can outlive it.
pub fn count_in_background(lines: Vec<String>) -> thread::JoinHandle<usize> {
thread::spawn(move || lines.len()) // `move`: the thread owns `lines`
}

A struct can hold a reference if it declares a lifetime parameter. The 'a says: a value of this struct cannot outlive the data that it borrows.

/// Reads fields from a line without copying it. The `'a` says: a
/// `LineParser` cannot outlive the text that it borrows.
pub struct LineParser<'a> {
rest: &'a str,
}
impl<'a> LineParser<'a> {
pub fn new(line: &'a str) -> Self {
Self { rest: line }
}
pub fn next_field(&mut self) -> Option<&'a str> {
if self.rest.is_empty() {
return None;
}
let (field, rest) = self.rest.split_once(',').unwrap_or((self.rest, ""));
self.rest = rest;
Some(field)
}
}

This is valid, and it is a common pattern for parsers, which read a large input without copying it. But it is a design choice with a cost: every function that stores or returns a LineParser must also handle 'a, and the parser cannot be kept after the input is freed. When you see a struct with a lifetime parameter in AI-generated code, ask: why does this struct not own its data? If the answer is “to save a copy of a short string”, an owned String is usually the better design.

  • No annotations where elision works. Idiomatic code has lifetime annotations only where the compiler needs them.
  • Owned data in long-lived structs. Structs that live a long time (configuration, domain objects, cache entries) own their fields. Borrowing structs are short-lived helpers: parsers, iterators, views.
  • 'static only on real constants, and T: 'static bounds only where the API needs them (threads, global registries).
  • Owned return values when the borrow is awkward. If a function returns a reference with a complex lifetime, an owned String or Vec can be simpler, and is often fast enough.
  • Annotations that elision makes unnecessary. fn first_word<'a>(text: &'a str) -> &'a str. Correct, but noise. Clippy reports it (needless_lifetimes). See S21: Unnecessary lifetime annotations, or 'static to silence errors.
  • Unused lifetime parameters. <'a, 'b> on a function where one of them is not used in the output. The agent guessed.
  • 'static added to make an error go away. The compiler says “borrowed value does not live long enough”, and the agent changes &'a str to &'static str. Now only literals work, and the next caller with a runtime string has a problem.
  • Box::leak to produce a &'static value. It compiles because the memory is never freed. In a loop or a server, this is a memory leak:
/// Makes the error go away by leaking memory: the `String` is never freed.
pub fn config_name(name: String) -> &'static str {
Box::leak(name.into_boxed_str())
}
  • A struct with a lifetime parameter that is stored for a long time, for example in a cache or in application state. The lifetime spreads to every type that holds it.

The task: two small helpers that read parts of an order line.

  1. WorseExplicit lifetimes that elision gives for free, and an unused parameter with its own lifetime.

    pub fn first_word<'a>(text: &'a str) -> &'a str {
    text.split_whitespace().next().unwrap_or("")
    }
    pub fn customer_name<'a, 'b>(order_line: &'a str, _separator: &'b str) -> String {
    order_line.split(',').nth(1).unwrap_or("").to_string()
    }
    Run in Playground (opens the Rust Playground in a new tab)
  2. BetterNo annotations: the elision rules give the same meaning. The unused parameter is gone.

    pub fn first_word(text: &str) -> &str {
    text.split_whitespace().next().unwrap_or("")
    }
    pub fn customer_name(order_line: &str) -> String {
    order_line.split(',').nth(1).unwrap_or("").to_string()
    }
    Run in Playground (opens the Rust Playground in a new tab)

Both versions compile, and the meaning of first_word is identical. The cost of the “worse” version is reading time, and a false signal: explicit lifetimes tell the reader “something unusual happens here”. When nothing unusual happens, the reader looks for a problem that is not there.

Explicit annotations are correct when the elision rules do not apply (two input references and a reference output, as in longer), or when the author wants to show that the result is tied to one specific input.

  1. What does 'a in fn longer<'a>(first: &'a str, second: &'a str) -> &'a str change at run time?

    Show the answer

    Nothing. Lifetimes exist only at compile time. The annotation tells the compiler that the result is valid only while both inputs are valid, so it can reject code that uses the result too late.

  2. Does fn name(&self) -> &str need a lifetime annotation?

    Show the answer

    No. With &self, the elision rules give the output the lifetime of self. The method returns a borrow of the object.

  3. Is a String value 'static?

    Show the answer

    As a type bound, yes: String: 'static is true, because a String borrows nothing. It does not live forever. It is freed when its owner goes out of scope.

  4. An agent fixed a lifetime error with Box::leak. What do you ask?

    Show the answer

    How often this code runs. Each call leaks memory that is never freed. Ask the agent to change the type that needs &'static str to own a String, or to restructure the code so that the value lives long enough.