Introduction

Lifetimes are Rust's compile-time mechanism for ensuring every reference points to valid memory. They are the foundation of Rust's memory safety guarantees without a garbage collector, and understanding them is essential for writing idiomatic Rust code.

Key Concepts

  • Lifetime: The region of code during which a reference is valid and safe to use.
  • Dangling reference: A reference that points to memory that has been freed, which Rust prevents at compile time.
  • Borrow checker: The compiler subsystem that compares lifetimes and rejects programs where references could outlive the data they point to.

Real World Context

Every non-trivial Rust program uses references, and every reference has a lifetime. When you return a reference from a function, store a reference in a struct, or pass references to threads, the borrow checker enforces that those references remain valid. Without lifetimes, Rust would need a garbage collector or allow use-after-free bugs.

Deep Dive

The most fundamental problem lifetimes solve is the dangling reference. Consider a function that tries to return a reference to a local variable:

rust
fn dangling() -> &String {
    let s = String::from("hello");
    &s  // Error! s is dropped when function returns
}     // Reference would point to freed memory

The borrow checker rejects this because s is dropped at the end of the function, but the returned reference would still try to point to it. This is a compile-time error, not a runtime crash.

Lifetimes represent the scope where a reference is valid. The borrow checker verifies that every reference's lifetime fits within the lifetime of the data it borrows:

rust
fn main() {
    let x = 5;            // ----------+-- 'a
    let r = &x;           // --+-- 'b  |
                          //   |       |
    println!("{}", r);    //   |       |
}                         // --+-------+
// 'b fits entirely within 'a, so this compiles

When lifetimes do not nest correctly, the compiler rejects the code:

rust
fn main() {
    let r;                  // ---------+-- 'a
    {                       //          |
        let x = 5;          // -+-- 'b  |
        r = &x;             //  |       |
    }                       // -+       |
    println!("{}", r);      // Error! 'b ended before use
}                           // ---------+

Here, r borrows x, but x is dropped before r is used. The borrow checker catches this mismatch.

Common Pitfalls

  1. Thinking lifetimes change how long values live — Lifetime annotations only describe relationships between references; they never extend or shorten the actual lifetime of any value.
  2. Forgetting that most lifetimes are inferred — You only need explicit annotations when the compiler cannot determine the relationships automatically, such as in functions with multiple reference parameters.

Best Practices

  1. Start without annotations — Let the compiler's elision rules handle lifetimes. Only add annotations when the compiler asks for them.
  2. Read lifetime errors carefully — The borrow checker's error messages tell you exactly which reference outlives which scope. Follow the spans it highlights.

Summary

  • Every reference in Rust has a lifetime that the borrow checker tracks.
  • Lifetimes prevent dangling references at compile time.
  • Annotations describe relationships between lifetimes but do not change how long data lives.
  • Most lifetimes are inferred automatically; explicit annotations are only needed when the compiler requests them.

Code Examples

rust
// Lifetime annotations don't change how long values live!
// They just describe relationships for the compiler.

// This tells the compiler: the return value lives as long
// as the shorter of 'a and 'b
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
✓ Completed