LearningRust.org
LessonsPlaygroundAbout
Sign In
Lessons/advanced/Lifetimes
PreviousPracticeNext

TL;DR

Rust lifetimes explained — learn lifetime annotations, elision rules, and how the borrow checker validates reference validity

Key concepts

  • Rust lifetimes explained
  • Rust lifetime annotations
  • Rust lifetime elision
  • Rust reference lifetimes
Loading...

Next lesson

Traits & Generics

Learn to write flexible, reusable code with Rust's trait system and generic programming

30 min

Related lessons

  • ConcurrencyLearn safe concurrent programming in Rust with threads, message passing, and shared state
  • Smart PointersUnderstand Box, Rc, RefCell, and interior mutability patterns in Rust
  • MacrosLearn how to use and create Rust macros for metaprogramming and code generation at compile time

Also learn

ZigLearn Zig ProgrammingTypeScriptMaster TypeScript

Also learn

ZigLearn Zig ProgrammingTypeScriptMaster TypeScript

A14A

Building digital products that matter.

© 2026 A14A. All rights reserved.
KVK: 87105004PrivacyTerms

Lifetimes

Lifetimes are Rust's way of ensuring that references are always valid. Every reference in Rust has a lifetime - the scope for which that reference is valid. Most of the time, lifetimes are inferred, but sometimes you need to annotate them explicitly.

What Are Lifetimes?

A lifetime is the scope during which a reference is valid. Consider this:

fn main() {
    let r;                // ---------+-- 'a
                          //          |
    {                     //          |
        let x = 5;        // -+-- 'b  |
        r = &x;           //  |       |
    }                     // -+       |
                          //          |
    // println!("{}", r); // ERROR: x doesn't live long enough
}                         // ---------+

The reference r has lifetime 'a, but it refers to x which only has lifetime 'b. Since 'b is shorter than 'a, the code won't compile.

Lifetime Annotation Syntax

Lifetime annotations describe relationships between lifetimes:

&i32        // a reference
&'a i32     // a reference with explicit lifetime 'a
&'a mut i32 // a mutable reference with explicit lifetime 'a

When You Need Lifetime Annotations

The compiler needs help when:

  1. A function returns a reference
  2. A struct holds references
  3. Multiple references have ambiguous relationships

Function Signatures

// This won't compile - Rust doesn't know which input's lifetime to use
// fn longest(x: &str, y: &str) -> &str {
//     if x.len() > y.len() { x } else { y }
// }

// Solution: annotate with lifetime 'a
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

fn main() {
    let string1 = String::from("long string");
    let string2 = String::from("short");

    let result = longest(&string1, &string2);
    println!("The longest string is: {}", result);
}

The 'a annotation means: "the returned reference will be valid for the smaller of the two input lifetimes."

Lifetime Elision Rules

Rust has rules for inferring lifetimes so you don't always need annotations:

Rule 1: Each Input Gets Its Own Lifetime

// Written:
fn first_word(s: &str) -> &str { ... }
// Compiler infers:
fn first_word<'a>(s: &'a str) -> &str { ... }

Rule 2: One Input Lifetime = Output Lifetime

// Written:
fn first_word(s: &str) -> &str { ... }
// Compiler infers:
fn first_word<'a>(s: &'a str) -> &'a str { ... }

Rule 3: &self Lifetime = Output Lifetime

impl MyStruct {
    // Written:
    fn get_name(&self) -> &str { ... }
    // Compiler infers:
    fn get_name<'a>(&'a self) -> &'a str { ... }
}

Structs with References

When a struct holds references, you must annotate lifetimes:

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

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

    let excerpt = ImportantExcerpt {
        part: first_sentence,
    };

    println!("Excerpt: {}", excerpt.part);
}

The annotation 'a means: "an instance of ImportantExcerpt can't outlive the reference it holds."

Methods with Lifetimes

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

impl<'a> ImportantExcerpt<'a> {
    // Lifetime elision: &self lifetime is used for return
    fn level(&self) -> i32 {
        3
    }

    // Return type uses 'a from struct
    fn announce_and_return_part(&self, announcement: &str) -> &'a str {
        println!("Attention please: {}", announcement);
        self.part
    }
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let excerpt = ImportantExcerpt {
        part: novel.split('.').next().unwrap(),
    };

    println!("Level: {}", excerpt.level());
    println!("Part: {}", excerpt.announce_and_return_part("Here it comes!"));
}

The Static Lifetime

'static means the reference lives for the entire program duration:

fn main() {
    // String literals have 'static lifetime
    let s: &'static str = "I live forever!";

    println!("{}", s);
}

Use 'static sparingly - it's usually a sign you should reconsider your design.

Multiple Lifetime Parameters

Sometimes you need multiple lifetime parameters:

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

fn main() {
    let s1 = String::from("hello");
    let s2 = String::from("world!");
    let ann = String::from("Comparing strings");

    let result = longest_with_announcement(&s1, &s2, &ann);
    println!("Longest: {}", result);
}

Lifetime Bounds

You can specify that a generic type must live at least as long as a lifetime:

fn print_ref<'a, T>(t: &'a T)
where
    T: std::fmt::Display + 'a,
{
    println!("{}", t);
}

fn main() {
    let x = 5;
    print_ref(&x);
}

Common Lifetime Patterns

Pattern 1: Input/Output Relationship

fn first_word<'a>(s: &'a str) -> &'a str {
    match s.find(' ') {
        Some(pos) => &s[..pos],
        None => s,
    }
}

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

Pattern 2: Struct Holding a Reference

struct Parser<'a> {
    input: &'a str,
    position: usize,
}

impl<'a> Parser<'a> {
    fn new(input: &'a str) -> Parser<'a> {
        Parser { input, position: 0 }
    }

    fn remaining(&self) -> &'a str {
        &self.input[self.position..]
    }
}

fn main() {
    let text = String::from("hello world");
    let parser = Parser::new(&text);
    println!("Remaining: {}", parser.remaining());
}

Pattern 3: Returning References from Methods

struct Container {
    data: Vec<String>,
}

impl Container {
    fn get(&self, index: usize) -> Option<&String> {
        self.data.get(index)
    }

    fn first(&self) -> Option<&String> {
        self.data.first()
    }
}

fn main() {
    let container = Container {
        data: vec![String::from("a"), String::from("b")],
    };

    if let Some(first) = container.first() {
        println!("First: {}", first);
    }
}

Practice Exercise

// A struct that borrows a string slice
struct Highlight<'a> {
    text: &'a str,
    start: usize,
    end: usize,
}

impl<'a> Highlight<'a> {
    fn new(text: &'a str, start: usize, end: usize) -> Highlight<'a> {
        Highlight { text, start, end }
    }

    fn highlighted_portion(&self) -> &'a str {
        &self.text[self.start..self.end]
    }

    fn full_text(&self) -> &'a str {
        self.text
    }
}

fn main() {
    let document = String::from("Rust is a systems programming language");

    let highlight = Highlight::new(&document, 0, 4);

    println!("Highlighted: '{}'", highlight.highlighted_portion());
    println!("Full text: '{}'", highlight.full_text());
}

Key Takeaways

  • Lifetimes ensure references are always valid
  • Most lifetimes are inferred by the compiler
  • Use 'a syntax when the compiler needs help
  • Lifetime annotations describe relationships, they don't change how long things live
  • Structs holding references need lifetime parameters
  • 'static means "lives for the entire program"
  • Lifetime elision rules reduce annotation boilerplate

Lifetimes are one of Rust's most powerful features for memory safety!

Next Steps

With lifetimes understood, you're ready for traits and generics — Rust's tools for writing flexible, reusable code that works across different types.

Ready to continue? Head to Traits and Generics!