Lifetimes

What Is a Lifetime

In Rust, a lifetime is the span of execution (region of code) for which a reference (&T or &mut T) is valid to use. Every reference in Rust has a lifetime, and the borrow checker compares the lifetime of each reference against the scope of the value it points to in order to guarantee that no reference can ever outlive the data it borrows.

Value Scope vs Reference Lifetime

To understand lifetimes, distinguish two spans in a program:

  1. The owner's scope (value lifetime): The span from where a value is created (let x = ...;) until it is moved or dropped (} or drop(x)).
  2. The reference's lifetime ('a): The span from where a borrow is created (let r = &x;) until the last line where that reference r is actually used.

For a program to be memory-safe, the reference's lifetime 'a must fit completely inside the owner's scope:

Valid Borrow (Reference lifetime `'a` is strictly inside owner `x`'s scope):

  fn main() {
      let x = 42;             ──┬── Owner `x` is alive
      let r = &x;         ──┬─  │   (Reference `r` lifetime `'a` starts)
      println!("{r}");    ──┴─  │   (Reference `r` lifetime `'a` ends after last use!)
      println!("{x}");          │
  }                           ──┴── Owner `x` is dropped at `}`
fn main() {
    let x = 42;
    let r: &i32 = &x; // Borrow starts here

    println!("Read via reference: {r}"); // Last use of `r` -> lifetime of `r` ends here!

    println!("Owner x is still valid: {x}");
} // `x` is dropped here
Read via reference: 42
Owner x is still valid: 42

What Happens When a Reference Outlives Its Referent (E0597)

If a reference r is declared in an outer scope, borrows a variable inner declared inside a shorter inner block { ... }, and is then read after the inner block ends, the reference's required lifetime extends past the point where inner was destroyed.

The compiler rejects this at compile time with E0597 (does not live long enough):

Invalid Borrow (Reference `r` lifetime extends past `inner`'s destruction!):

  fn main() {
      let r: &i32;            ──┬── `r` declared in outer scope
      {                         │
          let inner = 10; ──┬─  │   `inner` created
          r = &inner;       │   │   `r` borrows `inner`
      }                   ──┴─  │   ❌ `inner` is DROPPED here at `}`!
      println!("{r}");        ──┴── `r` is used here (would read freed stack memory!)
  }
// ❌ does not compile
fn main() {
    let r: &i32;

    {
        let inner = 10;
        r = &inner;
    } // `inner` goes out of scope and is dropped here!

    println!("r = {r}");
}
error[E0597]: `inner` does not live long enough
 --> src/main.rs:6:13
  |
5 |         let inner = 10;
  |             ----- binding `inner` declared here
6 |         r = &inner;
  |             ^^^^^^ borrowed value does not live long enough
7 |     }
  |     - `inner` dropped here while still borrowed
8 |
9 |     println!("r = {r}");
  |                    - borrow later used here

error: aborting due to 1 previous error

Non-Lexical Lifetimes (Lifetimes End at Last Use)

In early versions of Rust (before the 2018 edition), a reference's lifetime always lasted until the closing brace } of the block where the reference variable was declared.

In modern Rust, lifetimes are Non-Lexical (NLL): a reference's lifetime ends immediately after the last statement that uses the reference, even if the reference variable itself remains in lexical scope until the end of the function:

fn main() {
    let mut text = String::from("hello");

    let shared_ref: &String = &text;
    println!("Shared borrow read: {shared_ref}");
    // `shared_ref` is NEVER used again below this line!
    // Its lifetime ends right here, BEFORE the closing brace of `main`.

    // Because `shared_ref`'s lifetime has already ended, we can mutate `text`:
    text.push_str(", world");
    println!("Mutated text: {text}");
}
Shared borrow read: hello
Mutated text: hello, world

Lifetimes Are Purely a Compile-Time Concept (Zero Runtime Cost)

Lifetime names (written with an apostrophe, such as 'a, 'b, or 'static) exist exclusively during compilation:

  1. During MIR borrow checking, rustc solves a system of region constraints ('a: 'b, meaning 'a outlives 'b) to prove that every borrow is valid at every point it is dereferenced.
  2. Once borrow checking succeeds, all lifetime annotations are completely erased before LLVM code generation begins.
  3. A reference &'a i32 and a reference &'static i32 generate the exact same 8-byte machine pointer with zero runtime tracking metadata.