Lifetimes

Why Lifetimes Are Necessary

Lifetimes are necessary in Rust to prevent dangling pointers (use-after-free bugs) at compile time without needing a runtime garbage collector. Furthermore, explicit lifetime annotations ('a) are necessary on function signatures and struct definitions whenever a returned reference or stored reference could plausibly borrow from more than one source, allowing rustc to verify callers and callees independently.

Problem 1: Preventing Dangling References Locally

In C and C++, a pointer can easily outlive the stack variable or heap buffer it points to. If a function returns the address of a local stack variable (return &local_var;), the stack frame is destroyed on return and the caller receives a pointer to garbage memory.

By tracking the lifetime of every value and reference, Rust catches dangling references at compile time before the program can ever run:

// ❌ does not compile
fn create_dangling_reference() -> &String {
    let local = String::from("temporary stack String");
    &local // Error: `local` is dropped at `}`, so `&local` would dangle!
}

fn main() {}
error[E0106]: missing lifetime specifier
 --> src/main.rs:1:35
  |
1 | fn create_dangling_reference() -> &String {
  |                                   ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`, or if you really want a 'static reference
  |
1 | fn create_dangling_reference() -> &'static String {
  |                                    +++++++
help: instead, you are more likely to want to return an owned value
  |
1 - fn create_dangling_reference() -> &String {
1 + fn create_dangling_reference() -> String {
  |

error: aborting due to 1 previous error

Even if you annotate -> &'static String, the compiler rejects &local with E0515 (cannot return reference to local variable 'local'). Returning the owned String by value (-> String) transfers ownership safely to the caller instead.

Problem 2: Why Function Signatures Need Lifetime Annotations

Remember that Rust checks each function locally against its signature without inspecting the bodies of other functions.

Suppose you write a function longest(x: &str, y: &str) -> &str that takes two string slices and returns the longer one:

Caller Scope (`main`):
  let string1 = String::from("long string");   (Lives for outer scope `'long`)
  {
      let string2 = String::from("xyz");       (Lives only for inner scope `'short`)
      let result = longest(&string1, &string2);
  } // <-- `string2` is DROPPED here!
  println!("{result}"); // <-- Is `result` still valid here?

Look at longest(&string1, &string2) from the compiler's perspective when checking main:

  • If longest always returned its first argument (x), result would borrow string1 ('long) and println!("{result}") after the inner block would be safe.
  • If longest can return either x or y depending on runtime lengths, then result might point to string2 ('short), so using result after string2 is dropped would be a use-after-free bug!

Because rustc refuses to guess which input parameter (x or y) the returned &str borrows from by secretly reading longest's body, longest without lifetime annotations fails to compile:

// ❌ does not compile
fn longest(x: &str, y: &str) -> &str {
    if x.len() >= y.len() { x } else { y }
}

fn main() {}
error[E0106]: missing lifetime specifier
 --> src/main.rs:1:33
  |
1 | fn longest(x: &str, y: &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 `x` or `y`
help: consider introducing a named lifetime parameter
  |
1 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
  |           ++++     ++          ++          ++

error: aborting due to 1 previous error

How Lifetime Annotations Solve the Ambiguity

Adding <'a> to the signature (fn longest<'a>(x: &'a str, y: &'a str) -> &'a str) establishes an explicit contract between longest and its callers:

  • Contract on longest's body: The returned &str may come from either x or y.
  • Contract on callers of longest: The returned reference is guaranteed to remain valid for the overlap (shorter) of x's lifetime and y's lifetime!
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() >= y.len() { x } else { y }
}

fn main() {
    let string1 = String::from("long string is long");

    {
        let string2 = String::from("xyz");
        // Both `string1` and `string2` are alive inside this inner block, so `result` is valid here:
        let result = longest(&string1, &string2);
        println!("Longest string is: '{result}'");
    }
}
Longest string is: 'long string is long'

Problem 3: Why Structs Holding References Need Lifetimes

If a struct stores a reference field (such as struct Excerpt<'a> { text: &'a str }), the struct instance itself must never outlive the String that its text field borrows from. Annotating 'a on the struct allows the borrow checker to tie the struct's validity directly to the borrowed source buffer.