RustLifetimes

Lifetimes in Rust

Lifetimes are one of Rust's most distinctive features — and one that trips up newcomers more than almost anything else. The good news: lifetimes are not magic. They are annotations that help the borrow checker verify that every reference in your program is valid at the time it is used.

A lifetime does not change how long data lives. It is simply a label that describes an existing relationship: "this reference must not outlive that data."

The Problem: Dangling References

Before looking at the syntax, it helps to see the problem lifetimes solve. A dangling reference is a reference that points to memory that has already been freed. In languages without a borrow checker this is a silent, catastrophic bug. Rust's compiler refuses to compile code that would create one.

RUST
fn main() {
    let r;                  // declare r — no value yet

    {
        let x = 5;
        r = &x;             // r borrows x
    }                       // x is dropped here — r now dangles

    println!("{}", r);      // ERROR: x does not live long enough
}
error[E0597]: `x` does not live long enough
 --> src/main.rs:6:13
  |
5 |         let x = 5;
6 |         r = &x;
  |             ^^ borrowed value does not live long enough
7 |     }
  |     - `x` dropped here while still borrowed
9 |     println!("{}", r);
  |                    - borrow later used here
Note
The borrow checker compares the lifetime of the data (x) with the lifetime of the reference (r). Because x is dropped before r is used, the compiler rejects the program.
Visualising Lifetimes as Scopes

You can think of a lifetime as the span of code during which a reference is valid. The borrow checker ensures that no reference outlives the data it points to.

Text
// BAD — r outlives x

fn main() {
    let r;                      // ------+-- r's lifetime begins
    {                           //       |
        let x = 5;              // -+    |   x's lifetime begins
        r = &x;                 //  |    |
    }                           // -+    |   x dropped — r still alive!
    println!("{}", r);          // ------+   COMPILER ERROR
}

// GOOD — r lives within x

fn main() {
    let x = 5;                  // ------+-- x's lifetime begins
    let r = &x;                 //    ---+-- r's lifetime begins (nested inside x)
    println!("{}", r);          //       |   used here — x still alive: OK
}                               // ------+-- both dropped
Lifetime Annotations: The 'a Syntax

When the compiler cannot figure out the relationship between reference lifetimes on its own — most often in function signatures — you must annotate them explicitly.

Lifetime parameters begin with an apostrophe and are usually short lowercase names: 'a, 'b, 'input. They are placed after the & of a reference type:

RUST
&str          // a reference — no lifetime annotation
&'a str       // a reference with explicit lifetime 'a
&'a mut str   // a mutable reference with explicit lifetime 'a
Tip
Lifetime annotations are descriptive, not prescriptive. Writing'a does not create or lengthen a lifetime — it tells the compiler "whatever lifetime this reference has, call it 'a."
Lifetime Annotations in Function Signatures

The classic example is a function that returns the longer of two string slices. Without lifetime annotations the compiler cannot tell whether the returned reference comes from x or y, and therefore cannot verify that the caller uses it safely.

RUST
// This does NOT compile — the compiler cannot determine the return lifetime
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() { x } else { y }
}

// Correct version — annotated with 'a
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

The annotation <'a> says: "for some lifetime 'a, both x and y live at least as long as 'a, and the returned reference also lives at least as long as 'a."

In practice 'a is the shorter of the two input lifetimes — the borrow checker picks the most conservative valid region.

RUST
fn main() {
    let s1 = String::from("long string");
    let result;
    {
        let s2 = String::from("xyz");
        result = longest(s1.as_str(), s2.as_str());
        println!("longest: {}", result); // OK — result used inside s2's scope
    }
    // println!("{}", result); // would ERROR — s2 dropped, result could dangle
}

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
longest: long string
Lifetimes in Structs

Structs can hold references, but every reference field must have a lifetime annotation. This prevents the struct from outliving the data it references.

RUST
struct Important<'a> {
    part: &'a str,
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().expect("no period found");

    let excerpt = Important { part: first_sentence };
    println!("Important part: {}", excerpt.part);
}   // novel and excerpt are both dropped here — safe
Important part: Call me Ishmael

The annotation <'a> on Important says: "an Important instance cannot outlive the reference stored in its part field." If you tried to move excerpt out of the scope where novel lives, the compiler would reject it.

Lifetimes in impl Blocks

When implementing methods on a struct that has lifetime parameters, you must declare the lifetime in the impl block and repeat it after the struct name.

RUST
struct Important<'a> {
    part: &'a str,
}

impl<'a> Important<'a> {
    // Method that returns a reference — lifetime elision applies (see next page)
    fn announce(&self, announcement: &str) -> &str {
        println!("Attention: {}!", announcement);
        self.part
    }
}

fn main() {
    let text = String::from("some important text");
    let imp = Important { part: &text };
    let part = imp.announce("listen up");
    println!("The important part: {}", part);
}
Attention: listen up!
The important part: some important text
The 'static Lifetime

The special lifetime 'static means the reference is valid for the entire duration of the program. String literals are the most common example — they are stored directly in the program's binary and therefore always available.

RUST
// String literals have type &'static str
let s: &'static str = "I live forever";

// Functions can require 'static when storing references long-term
fn needs_static(s: &'static str) {
    println!("{}", s);
}

fn main() {
    needs_static("hello");          // OK — string literal is 'static

    let owned = String::from("world");
    // needs_static(&owned);        // ERROR — &owned is not 'static
}
Warning
When the compiler suggests adding 'static to fix a lifetime error, think carefully before doing so. It usually means your design needs to own the data (e.g. use String instead of &str) rather than keep a reference around indefinitely.
Multiple Lifetime Parameters

A function can have multiple independent lifetime parameters. This is useful when the return type is tied to only one of the inputs.

RUST
// The return value's lifetime is tied to x, not y
fn mixed<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
    println!("y is: {}", y);
    x
}

fn main() {
    let s1 = String::from("hello");
    let result;
    {
        let s2 = String::from("world");
        result = mixed(s1.as_str(), s2.as_str());
        // s2 can be dropped because result's lifetime only depends on s1
    }
    println!("result: {}", result); // OK — s1 still alive
}
y is: world
result: hello
Tip
Use multiple lifetime parameters when you want to express that the return value borrows from only a specific argument, freeing the other arguments to be dropped sooner.
Lifetime Bounds on Generics

When combining generics with references you sometimes need to constrain the generic type to outlive a given lifetime. The syntax is T: 'a, which means "T must live at least as long as 'a."

RUST
use std::fmt::Display;

// T must implement Display and live at least as long as 'a
fn print_ref<'a, T>(val: &'a T)
where
    T: Display + 'a,
{
    println!("{}", val);
}

struct Wrapper<'a, T: 'a> {
    value: &'a T,
}

fn main() {
    let number = 42;
    print_ref(&number);

    let w = Wrapper { value: &number };
    println!("wrapped: {}", w.value);
}
42
wrapped: 42
Common Misconceptions
  • Lifetimes do not change how long data lives. The compiler never extends or shortens the actual memory lifetime of a value — it only checks that references are used correctly within existing lifetimes.

  • Lifetime annotations are not runtime overhead. They exist entirely in the type system and are erased before compilation to machine code.

  • You do not always need to write lifetimes. The compiler infers them in many common patterns via lifetime elision (covered on the next page).

  • A longer lifetime is not always better. Tying a reference to a longer lifetime can prevent data from being freed, causing unnecessary memory use.

  • 'static is not a magic fix. If the compiler complains about lifetimes, making everything 'static sidesteps the issue but usually indicates a design problem.

Combining Lifetimes, Generics, and Trait Bounds

All three features compose naturally. Here is a function that uses generics, a trait bound, and a lifetime together — a pattern common in real-world Rust libraries.

RUST
use std::fmt::Display;

fn longest_with_announcement<'a, T>(
    x: &'a str,
    y: &'a str,
    announcement: T,
) -> &'a str
where
    T: Display,
{
    println!("Announcement: {}", announcement);
    if x.len() > y.len() { x } else { y }
}

fn main() {
    let s1 = String::from("long string");
    let s2 = String::from("xyz");
    let result = longest_with_announcement(
        s1.as_str(),
        s2.as_str(),
        "Today's winner is...",
    );
    println!("Longest: {}", result);
}
Announcement: Today's winner is...
Longest: long string
How the Borrow Checker Uses Lifetimes

The borrow checker's job is to ensure that for every reference in your program, the data it points to is still alive. It does this by:

  1. Assigning a lifetime to every reference (either inferred or from your annotation).
  2. Checking that the region where the reference is used is contained within the lifetime of the data.
  3. Rejecting the program if any reference could outlive its data.

Lifetime annotations in function signatures give the borrow checker the information it needs to verify call sites without looking at the function body — only at the signature.

Text
Borrow checker flow for longest<'a>(x: &'a str, y: &'a str) -> &'a str

  Call site:      longest(s1.as_str(), s2.as_str())
                        |                  |
  Lifetimes:      'a = lifetime of s1    'a = lifetime of s2
                  -> 'a is the SHORTER of the two
                  -> return value must be used within that shorter region

  Checker verifies: is the return value used only while both s1 and s2 are alive?
  If yes: compile. If no: error.
Success
Once you understand that lifetime annotations are just a way to name and connect reference scopes — not a way to change them — the borrow checker starts to feel like a helpful collaborator rather than an obstacle.
Quick Reference

Pattern

Syntax

Meaning

Lifetime parameter

fn f<'a>(x: &'a str)

'a names the lifetime of x

Multiple lifetimes

fn f<'a, 'b>(x: &'a str, y: &'b str)

x and y may have different lifetimes

Struct with reference

struct S<'a> { f: &'a str }

S cannot outlive its field

impl block

impl<'a> S<'a> { ... }

Lifetime must be declared on impl

Static lifetime

&'static str

Reference valid for entire program

Lifetime bound

T: 'a

T must live at least as long as a

Multiple bounds

T: Display + 'a

T implements Display and lives >= a