Skip to main content
Rust intermediate Lesson 10 of 30

Lifetimes in Rust

Understand lifetime annotations, the 'static lifetime, lifetime elision, and lifetimes in structs.

What Are Lifetimes?

Every reference in Rust has a lifetime — the scope for which the reference is valid. Most of the time the compiler infers lifetimes automatically. You only need to write explicit lifetime annotations when the compiler cannot figure out the relationship between the lifetimes of multiple references on its own.

Lifetimes prevent dangling references:

fn main() {
    let r;
    {
        let x = 5;
        r = &x; // ERROR: x does not live long enough
    }
    // println!("{}", r); // r would point to freed memory
}

The compiler sees that x is dropped before r is used and rejects the code.

Lifetime Annotation Syntax

Lifetime parameters are written with a leading apostrophe: 'a, 'b, 'input, etc. They appear after & in reference types and in angle brackets on function/struct signatures.

// Without annotation (may not compile for multi-reference returns)
// fn longest(x: &str, y: &str) -> &str { ... }

// With explicit lifetime annotation
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

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
    }
}

The annotation 'a says: “the returned reference lives at least as long as the shorter of x and y’s lifetimes.” The compiler uses this to reject code where the return value would outlive one of the inputs.

Lifetime Elision

Rust has three elision rules that let the compiler infer lifetimes without explicit annotations in common cases:

Rule 1: Each reference parameter gets its own lifetime.

fn foo(x: &str) -> &str   // expands to: fn foo<'a>(x: &'a str) -> &'a str

Rule 2: If there is exactly one input lifetime, it is assigned to all output lifetimes.

fn first_word(s: &str) -> &str {
    // compiler infers: fn first_word<'a>(s: &'a str) -> &'a str
    let i = s.find(' ').unwrap_or(s.len());
    &s[..i]
}

Rule 3: If one of the inputs is &self or &mut self, its lifetime is assigned to all output lifetimes.

impl MyStruct {
    fn get_name(&self) -> &str { // compiler infers output lifetime = 'self
        &self.name
    }
}

If none of these rules fully determine the output lifetimes, the compiler asks you to annotate explicitly.

Lifetimes in Structs

If a struct holds a reference, it must have a lifetime annotation to guarantee the struct doesn’t outlive the data it references:

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

impl<'a> Important<'a> {
    fn announce(&self, topic: &str) -> &str {
        println!("Attention regarding {}: {}", topic, self.text);
        self.text
    }
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence;
    {
        let i = novel.find('.').unwrap_or(novel.len());
        first_sentence = &novel[..i];
    }
    let important = Important { text: first_sentence };
    println!("{}", important.text);
}

The lifetime 'a ensures that Important cannot outlive the &str it holds.

Multiple Lifetime Parameters

When a function has references with independent lifetimes, you can use multiple parameters:

fn first_or_default<'a, 'b>(slice: &'a [&'b str], default: &'b str) -> &'b str {
    slice.first().copied().unwrap_or(default)
}

Keep annotations minimal — only introduce new lifetime parameters when the relationships genuinely differ.

The 'static Lifetime

'static means the reference is valid for the entire program:

// String literals are baked into the binary — always 'static
let s: &'static str = "I have a static lifetime";

// Functions that require 'static (e.g., thread::spawn)
fn needs_static(s: &'static str) {
    println!("{}", s);
}

Common sources of 'static data:

  • String literals
  • Box::leak — leaks heap memory to produce a 'static reference (use sparingly)
  • lazy_static! / once_cell — static initializers

Do not use 'static bounds just to avoid thinking about lifetimes — it is often too restrictive.

Lifetime Bounds on Generics

You can require that a generic type’s references live at least as long as a given lifetime:

use std::fmt::Display;

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

Common Lifetime Errors and Fixes

Error: returning a reference to local data

// WRONG
fn bad() -> &str {
    let s = String::from("hello");
    &s // s is dropped here!
}

// FIX: return owned data
fn good() -> String {
    String::from("hello")
}

Error: struct holds reference that outlives the struct

// WRONG — compiler asks for lifetime annotation
struct Excerpt {
    text: &str, // ERROR: missing lifetime specifier
}

// FIX: add lifetime parameter
struct Excerpt<'a> {
    text: &'a str,
}

Error: conflating lifetimes that should be independent

// This says: output lives as long as BOTH inputs — too restrictive
fn first_char<'a>(s: &'a str, _other: &'a str) -> &'a str {
    &s[..1]
}

// Better: output only depends on s
fn first_char_fixed<'a>(s: &'a str, _other: &str) -> &'a str {
    &s[..1]
}

Lifetimes are Not Runtime Costs

Lifetime annotations are purely compile-time constructs. They generate zero machine code. They are the compiler’s way of tracking how long references are valid, not runtime metadata.

Summary

ConceptMeaning
'aA named lifetime parameter
fn f<'a>(x: &'a T) -> &'a TOutput reference lives as long as input
struct S<'a> { r: &'a T }Struct cannot outlive the reference it holds
'staticReference valid for the entire program
ElisionCompiler infers lifetimes for common patterns
No annotation neededWhen elision rules fully determine all lifetimes

Frequently Asked Questions

What are lifetimes in Rust?
Lifetimes are annotations that tell the compiler how long references are valid. They don't change how long data lives — they just help the compiler verify that references don't outlive the data they point to.
What is lifetime elision?
Lifetime elision is a set of rules that let the compiler infer lifetimes in common patterns, so you don't have to write them explicitly most of the time.
What is the 'static lifetime?
'static means the reference is valid for the entire program lifetime. String literals have type &'static str because they are baked into the binary.