4 min read
On this page

Lifetimes

Lifetimes are how Rust knows that every reference points to valid data. They are not a new concept — every reference in every language has a lifetime. Rust just makes you think about them explicitly when the compiler cannot figure them out on its own. Most of the time, lifetimes are inferred. When they are not, the annotation syntax is straightforward once you understand what it means.

What Lifetimes Are

A lifetime is the scope during which a reference is valid. The compiler tracks these automatically in most cases:

fn main() {
    let r;                     // r declared here
    {
        let x = 5;
        r = &x;               // r borrows x
    }                          // x is dropped here
    // println!("{}", r);      // error: x does not live long enough
}

The reference r would outlive the data it points to. The compiler rejects this. No dangling pointer, no segfault, no undefined behavior.

When the compiler can see both the reference and the data it points to, it handles lifetimes automatically. Problems arise when references cross function boundaries — the compiler needs help knowing how long the returned reference is valid.

The 'a Notation

Lifetime annotations use a tick followed by a name: 'a, 'b, 'input, etc. They do not change how long data lives. They describe relationships between reference lifetimes so the compiler can verify safety.

// This says: the returned reference lives as long as both input references
fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str {
    if s1.len() > s2.len() {
        s1
    } else {
        s2
    }
}

fn main() {
    let s1 = String::from("long string");
    let result;
    {
        let s2 = String::from("hi");
        result = longer(&s1, &s2);
        println!("Longer: {}", result); // works: both s1 and s2 are alive
    }
    // println!("{}", result); // would fail: s2 is dropped, result might refer to it
}
Longer: long string

The 'a in longer means: the returned reference is valid for the shorter of the two input lifetimes. The compiler uses this to verify that you never use the result after either input is dropped.

Lifetime Elision

You do not need to annotate lifetimes most of the time. The compiler applies three elision rules:

  1. Each reference parameter gets its own lifetime.
  2. If there is exactly one input lifetime, it is assigned to all output references.
  3. If one of the parameters is &self or &mut self, its lifetime is assigned to all output references.
// You write this:
fn first_word(s: &str) -> &str {
    s.split_whitespace().next().unwrap_or("")
}

// The compiler sees this (rule 1 + rule 2):
// fn first_word<'a>(s: &'a str) -> &'a str

// You write this:
fn first_char(s: &str) -> Option<char> {
    s.chars().next()
}
// No lifetime annotation needed — return type contains no references

fn main() {
    let text = String::from("hello world");
    let word = first_word(&text);
    println!("{}", word);
}
hello

Elision covers the vast majority of cases. You only need explicit annotations when the compiler cannot determine the relationship between input and output lifetimes.

When Lifetimes Get Explicit

Lifetimes become necessary when a function takes multiple references and returns one:

// Won't compile without lifetimes — compiler doesn't know which
// input the output is tied to:
// fn pick(a: &str, b: &str, use_first: bool) -> &str

// With lifetimes:
fn pick<'a>(a: &'a str, b: &'a str, use_first: bool) -> &'a str {
    if use_first { a } else { b }
}

fn main() {
    let a = String::from("alpha");
    let b = String::from("beta");
    let chosen = pick(&a, &b, true);
    println!("{}", chosen);
}
alpha

If the output can only come from one input, you can be more precise:

// The return value only ever comes from `text`, not `prefix`
fn strip_prefix<'a>(text: &'a str, prefix: &str) -> &'a str {
    text.strip_prefix(prefix).unwrap_or(text)
}

fn main() {
    let text = String::from("hello_world");
    let result;
    {
        let prefix = String::from("hello_");
        result = strip_prefix(&text, &prefix);
    } // prefix dropped, but that's fine — result refers to text
    println!("{}", result);
}
world

Here, prefix has a different (shorter) lifetime than text, and that is fine because the return value is tied only to text.

Structs Holding References

Structs can hold references, but they must have lifetime annotations:

#[derive(Debug)]
struct Excerpt<'a> {
    text: &'a str,
}

impl<'a> Excerpt<'a> {
    fn new(text: &'a str) -> Self {
        Excerpt { text }
    }

    fn words(&self) -> Vec<&str> {
        self.text.split_whitespace().collect()
    }
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().unwrap();
    let excerpt = Excerpt::new(first_sentence);
    println!("{:?}", excerpt);
    println!("Words: {:?}", excerpt.words());
}
Excerpt { text: "Call me Ishmael" }
Words: ["Call", "me", "Ishmael"]

The lifetime 'a on Excerpt means: this struct cannot outlive the string it references. If novel is dropped while excerpt still exists, the compiler catches it.

The 'static Lifetime

'static means the reference lives for the entire program. String literals have this lifetime:

fn get_greeting() -> &'static str {
    "Hello, world!" // string literal, baked into the binary
}

// Leaked heap data also gets 'static lifetime (use sparingly):
fn leaked_string() -> &'static str {
    let s = String::from("I live forever");
    Box::leak(s.into_boxed_str())
}

fn main() {
    println!("{}", get_greeting());
    println!("{}", leaked_string());
}
Hello, world!
I live forever

'static does not mean immortal or immutable — it means the reference is valid for as long as it could possibly be needed. Trait objects sent across threads often require 'static because the compiler cannot know when the thread will finish.

The Pragmatic Solution: Own the Data

When lifetimes get complex, the simplest fix is often to stop borrowing and start owning:

// Complex: struct borrows data, lifetime annotations everywhere
struct BorrowedConfig<'a> {
    host: &'a str,
    port: u16,
}

// Simple: struct owns its data, no lifetimes needed
struct OwnedConfig {
    host: String,
    port: u16,
}

// In practice, OwnedConfig is almost always what you want.
// The cost of owning a String vs borrowing a &str is negligible
// for configuration data.

fn load_config() -> OwnedConfig {
    OwnedConfig {
        host: String::from("localhost"),
        port: 8080,
    }
}

fn main() {
    let config = load_config();
    println!("{}:{}", config.host, config.port);
}
localhost:8080

Borrowing in structs is useful for performance-critical code that processes large data without copying. For most application code, owning the data is simpler and the performance difference is irrelevant.

Lifetime Bounds on Generics

Generic types can have lifetime bounds:

use std::fmt::Display;

fn announce<'a, T: Display>(value: &'a T, label: &str) -> &'a T {
    println!("{}: {}", label, value);
    value
}

fn main() {
    let number = 42;
    let result = announce(&number, "The answer");
    println!("Got back: {}", result);
}
The answer: 42
Got back: 42

Real-World Example: Parsing Without Allocation

Lifetimes enable zero-copy parsing — you parse a large input and return references into the original data without allocating new strings:

#[derive(Debug)]
struct HttpHeader<'a> {
    name: &'a str,
    value: &'a str,
}

fn parse_header(line: &str) -> Option<HttpHeader<'_>> {
    let colon = line.find(':')?;
    Some(HttpHeader {
        name: line[..colon].trim(),
        value: line[colon + 1..].trim(),
    })
}

fn parse_headers(raw: &str) -> Vec<HttpHeader<'_>> {
    raw.lines()
        .filter_map(parse_header)
        .collect()
}

fn main() {
    let raw = "Content-Type: application/json\nContent-Length: 42\nHost: example.com";
    let headers = parse_headers(raw);
    for h in &headers {
        println!("{}: {}", h.name, h.value);
    }
}
Content-Type: application/json
Content-Length: 42
Host: example.com

No strings were allocated. Every name and value is a reference into the original raw string. For a high-performance HTTP parser, this matters.

Common Pitfalls

  • Adding lifetimes when you should own the data. If a struct lives longer than the data it references, you need String not &str. Most application structs should own their data.
  • Thinking lifetimes change how long data lives. They do not. They only describe relationships. Adding 'a does not extend or shorten anything.
  • Using 'static as an escape hatch. Requiring 'static on everything defeats the purpose of Rust's lifetime system. It usually means you should rethink your data ownership.
  • Panicking when you see complex lifetime errors. Read the error message. The compiler tells you which reference outlives which data. Follow the trail.
  • Over-annotating lifetimes. If elision handles it, do not add explicit annotations. They add noise without adding information. Let the compiler do its job.

Key Takeaways

  • Lifetimes ensure every reference points to valid data. They are checked at compile time with zero runtime cost.
  • Lifetime elision handles most cases automatically. You only annotate when the compiler asks.
  • The 'a syntax describes relationships between references — it does not control how long data lives.
  • Structs holding references need lifetime annotations, but consider owning the data instead.
  • 'static means the reference is valid for the entire program. String literals are 'static.
  • When lifetimes get complicated, the pragmatic solution is often to clone or own the data. Profile before optimizing with borrows.