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:
rustfn 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:
rustfn 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:
rustfn 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
- 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.
- 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
- Start without annotations — Let the compiler's elision rules handle lifetimes. Only add annotations when the compiler asks for them.
- 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
// 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 }
}