Lifetimes in Rust

December 22, 2025

In this post, we'll explore lifetimes in Rust - a unique feature that ensures references remain valid throughout your program's execution.

Before diving into lifetimes, make sure you're comfortable with these foundational concepts:

Lifetimes: Validating References

Before We Start: What Problem Are We Solving?

Remember references from Chapter 4? A reference is like a pointer, it points to data stored somewhere else.

let x = 5;
let r = &x;  // r is a reference to x

Here, r doesn't own the value 5. It just points to where x stores it.

But what happens if x goes away while r still exists?

let r;
{
    let x = 5;
    r = &x;
}  // x is dropped here!

println!("{}", r);  // PROBLEM! r points to... nothing!

This is called a dangling reference: a reference that points to memory that no longer exists. In other languages, this causes crashes, security bugs, and mysterious errors.

Rust prevents this entirely. The code above won't even compile.

Lifetimes are HOW Rust prevents dangling references.


Step 1: What Is a Lifetime?

A lifetime is the scope during which a reference is valid.

Simple Analogy: Library Books

Imagine you borrow a book from the library.

You can only read the book while you have it borrowed. After the due date, the book goes back. If you try to read it after returning it, you can't, because you don't have it anymore.

Lifetimes work the same way:

Another Analogy: Apartment Leases

Think of it like renting an apartment:

You can live in the apartment while your lease is valid. But if the building gets demolished (data dropped), your lease doesn't matter, the apartment doesn't exist anymore!

Rust's job is to make sure you never try to enter an apartment that's been demolished.


Step 2: The Borrow Checker

Rust has a part of the compiler called the borrow checker. Its job is to make sure all references are valid.

The borrow checker compares:

  1. How long the DATA lives
  2. How long the REFERENCE lives

If the reference could outlive the data, Rust rejects your code.

Example: Valid Reference

fn main() {
    let x = 5;         // x comes into scope
    let r = &x;        // r borrows x
    println!("{}", r); // use r, x is still alive, OK!
}                      // x and r both go out of scope

This works because:

Example: Invalid Reference (Dangling)

fn main() {
    let r;             // r comes into scope
    {
        let x = 5;     // x comes into scope
        r = &x;        // r borrows x
    }                  // x goes out of scope, DROPPED!
    println!("{}", r); // ERROR! r points to dropped data
}

This fails because:

Rust sees this and says: "Nope! You can't use r after x is gone."

The Error Message

error[E0597]: `x` does not live long enough
 --> src/main.rs:5:13
  |
5 |         r = &x;
  |             ^^ borrowed value does not live long enough
6 |     }
  |     - `x` dropped here while still borrowed
7 |     println!("{}", r);
  |                    - borrow later used here

Rust tells you exactly what's wrong:


Step 3: Lifetimes Are Usually Invisible

Here's the good news: most of the time, you don't write lifetimes yourself.

Rust figures them out automatically. This is called lifetime elision (we'll cover the rules later).

You've been writing code with lifetimes all along, Rust just handled them for you!

fn first_word(s: &str) -> &str {
    // Lifetimes are here, but Rust figures them out
    &s[0..1]
}

So when DO you need to write lifetimes explicitly?


Step 4: When You Need Explicit Lifetimes

You need to write lifetime annotations when Rust can't figure out how lifetimes relate.

The most common case: a function that takes multiple references and returns a reference.

The Problem

fn longer(s1: &str, s2: &str) -> &str {
    if s1.len() > s2.len() {
        s1
    } else {
        s2
    }
}

This won't compile! Rust says:

error[E0106]: missing lifetime specifier
 --> src/main.rs:1:33
  |
1 | fn longer(s1: &str, s2: &str) -> &str {
  |               ----      ----     ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but the
          signature does not say whether it is borrowed from `s1` or `s2`

Why Can't Rust Figure This Out?

Look at the function:

fn longer(s1: &str, s2: &str) -> &str {
    if s1.len() > s2.len() {
        s1      // Could return s1
    } else {
        s2      // Could return s2
    }
}

The return value could be EITHER s1 OR s2. Rust doesn't know which one!

Why does this matter? Because s1 and s2 might have DIFFERENT lifetimes:

fn main() {
    let string1 = String::from("hello");
    let result;
    {
        let string2 = String::from("world!");
        result = longer(&string1, &string2);
    }  // string2 is dropped here!
    
    println!("{}", result);  // Is this safe? It depends on which one was returned!
}

If longer returns s1 (pointing to string1), we're OK, string1 is still alive.

If longer returns s2 (pointing to string2), we have a dangling reference, string2 is gone!

Rust needs to know how the lifetimes relate so it can check if the code is safe.


Step 5: Lifetime Annotation Syntax

Lifetime annotations tell Rust how lifetimes relate to each other.

The Basic Syntax

Lifetime annotations start with an apostrophe ' followed by a name:

'a      // a lifetime called "a"
'b      // a lifetime called "b"
'hello  // a lifetime called "hello" (unusual but valid)

By convention, we use short lowercase names: 'a, 'b, 'c, etc.

Annotating References

You put the lifetime between the & and the type:

&i32        // a reference (no explicit lifetime)
&'a i32     // a reference with lifetime 'a
&'a mut i32 // a mutable reference with lifetime 'a

Breaking Down the Syntax

&'a i32
^
Reference symbol
&'a i32
 ^^
 Lifetime annotation
&'a i32
    ^^^
    The type being referenced

Step 6: Lifetime Annotations in Functions

Let's fix our longer function.

The Fix

fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str {
    if s1.len() > s2.len() {
        s1
    } else {
        s2
    }
}

Now it compiles! Let's break down every piece.

Breaking Down the Syntax

Step 1: Declare the lifetime parameter

fn longer<'a>(...)
//       ^^^^
//       Declares a lifetime parameter called 'a
//       Just like <T> declares a type parameter!

Step 2: Annotate the first parameter

fn longer<'a>(s1: &'a str, ...)
//            ^^^^^^^^
//            s1 is a reference with lifetime 'a

Step 3: Annotate the second parameter

fn longer<'a>(..., s2: &'a str) -> ...
//                 ^^^^^^^^
//                 s2 is ALSO a reference with lifetime 'a

Step 4: Annotate the return type

fn longer<'a>(...) -> &'a str
//                    ^^^^^^^
//                    The return value ALSO has lifetime 'a

What Does This Mean?

By using the SAME lifetime 'a for everything, we're saying:

"The returned reference will be valid for as long as BOTH input references are valid."

In other words:

Why the Shorter Lifetime?

Think about it:

fn main() {
    let string1 = String::from("hello");      // Lives longer
    {
        let string2 = String::from("world");  // Lives shorter
        let result = longer(&string1, &string2);
        println!("{}", result);  // OK here, both strings alive
    }
    // Can't use result here, string2 is gone
    // Even if result points to string1, Rust can't be sure
}

Rust takes the pessimistic view: the result might be from EITHER input, so it's only safe for the SHORTER lifetime.


Step 7: Lifetime Annotations Don't Change Anything

This is crucial to understand:

Lifetime annotations don't change how long anything lives.

They don't extend lifetimes. They don't shorten lifetimes. They just DESCRIBE the relationships between lifetimes.

Analogy

Think of lifetime annotations like labels on a map.

The map doesn't create rivers or mountains, it just describes where they are. Similarly, lifetime annotations don't create or modify lifetimes, they just describe how they relate.

You're telling Rust: "Hey, these lifetimes are connected like THIS." Then Rust checks if your code actually matches what you said.


Step 8: How Rust Uses Lifetime Annotations

Let's see how Rust uses our annotations to catch bugs.

Safe Code

fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str {
    if s1.len() > s2.len() { s1 } else { s2 }
}

fn main() {
    let string1 = String::from("hello");
    let string2 = String::from("world!");
    
    let result = longer(&string1, &string2);
    
    println!("Longer: {}", result);  // OK!
}

This works because:

Unsafe Code Caught

fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str {
    if s1.len() > s2.len() { s1 } else { s2 }
}

fn main() {
    let string1 = String::from("hello");
    let result;
    {
        let string2 = String::from("world!");
        result = longer(&string1, &string2);
    }  // string2 dropped here
    
    println!("Longer: {}", result);  // ERROR!
}

Rust says:

error[E0597]: `string2` does not live long enough

Here's what Rust figured out:

  1. longer returns something with lifetime 'a
  2. 'a is the shorter of the two input lifetimes
  3. string2 has a shorter lifetime (inner block only)
  4. So result might only be valid for the inner block
  5. But you try to use result after the inner block
  6. REJECTED!

Step 9: Different Lifetime Parameters

Sometimes different parameters have different lifetimes.

Example

What if you always return the FIRST parameter?

fn first<'a>(s1: &'a str, s2: &str) -> &'a str {
    s1  // Always returns s1
}

Breaking This Down

fn first<'a>(s1: &'a str, s2: &str) -> &'a str
//           ^^^^^^^^
//           s1 has lifetime 'a
fn first<'a>(s1: &'a str, s2: &str) -> &'a str
//                        ^^^^^
//                        s2 has NO lifetime annotation (it gets its own)
fn first<'a>(s1: &'a str, s2: &str) -> &'a str
//                                     ^^^^^^^
//                                     Return has lifetime 'a (same as s1)

What This Means

We're telling Rust: "The return value is connected to s1's lifetime, not s2's."

Now Rust knows the return value only needs s1 to stay alive, s2 can go away without causing problems.

fn first<'a>(s1: &'a str, s2: &str) -> &'a str {
    s1
}

fn main() {
    let string1 = String::from("hello");
    let result;
    {
        let string2 = String::from("world");
        result = first(&string1, &string2);
    }  // string2 dropped, but that's OK!
    
    println!("{}", result);  // Works! result only depends on string1
}

Step 10: Lifetime Annotations in Structs

What if a struct holds a reference? You need lifetime annotations!

The Problem

struct Excerpt {
    text: &str,  // ERROR!
}

This doesn't compile:

error[E0106]: missing lifetime specifier
 --> src/main.rs:2:11
  |
2 |     text: &str,
  |           ^ expected named lifetime parameter

Why?

The struct holds a reference. Rust needs to know: how long does that reference need to be valid?

The Fix

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

Breaking It Down

struct Excerpt<'a> {
//            ^^^^
//            Declare a lifetime parameter for the struct
struct Excerpt<'a> {
    text: &'a str,
//        ^^^^^^^
//        The text field has lifetime 'a
}

What This Means

The struct Excerpt<'a> contains a reference that must live at least as long as 'a.

Or to put it another way: An Excerpt cannot outlive the data it references.

Using a Struct with Lifetimes

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

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    
    let first_sentence = &novel[0..16];  // "Call me Ishmael"
    
    let excerpt = Excerpt {
        text: first_sentence,
    };
    
    println!("Excerpt: {}", excerpt.text);
}

This works because:

When It Breaks

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

fn main() {
    let excerpt;
    {
        let novel = String::from("Call me Ishmael. Some years ago...");
        excerpt = Excerpt {
            text: &novel[0..16],
        };
    }  // novel is dropped!
    
    println!("Excerpt: {}", excerpt.text);  // ERROR!
}

Rust catches this:

error[E0597]: `novel` does not live long enough

The Excerpt would outlive the data it references, not allowed!


Step 11: Methods on Structs with Lifetimes

When you implement methods on a struct with lifetimes, you need to declare the lifetime on the impl block.

The Syntax

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

impl<'a> Excerpt<'a> {
    fn text(&self) -> &str {
        self.text
    }
}

Breaking It Down

impl<'a> Excerpt<'a> {
// ^^^^
// Declare the lifetime parameter
impl<'a> Excerpt<'a> {
//       ^^^^^^^^^^^
//       Implementing for Excerpt with that lifetime

This is just like generics! Remember how we wrote impl<T> Holder<T>? Same pattern with lifetimes: impl<'a> Excerpt<'a>.

Full Example

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

impl<'a> Excerpt<'a> {
    // Return the text
    fn text(&self) -> &str {
        self.text
    }
    
    // Return the length
    fn len(&self) -> usize {
        self.text.len()
    }
    
    // Check if it contains a word
    fn contains(&self, word: &str) -> bool {
        self.text.contains(word)
    }
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let excerpt = Excerpt { text: &novel[0..16] };
    
    println!("Text: {}", excerpt.text());      // Call me Ishmael
    println!("Length: {}", excerpt.len());     // 16
    println!("Has 'me': {}", excerpt.contains("me")); // true
}

Step 12: Lifetime Elision Rules

Remember I said Rust usually figures out lifetimes automatically? Here's how.

Rust has three lifetime elision rules. The compiler applies these rules to figure out lifetimes without you writing them.

Rule 1: Each Reference Parameter Gets Its Own Lifetime

When you write:

fn foo(x: &str, y: &str) { }

Rust automatically adds:

fn foo<'a, 'b>(x: &'a str, y: &'b str) { }

Each reference parameter gets its own unique lifetime.

Rule 2: One Input Lifetime → Output Gets That Lifetime

When you write:

fn foo(x: &str) -> &str { }

Rust automatically adds:

fn foo<'a>(x: &'a str) -> &'a str { }

If there's exactly ONE input lifetime, the output gets the same lifetime.

Rule 3: &self or &mut self → Output Gets Self's Lifetime

When you write:

impl MyStruct {
    fn foo(&self, x: &str) -> &str { }
}

Rust automatically adds:

impl MyStruct {
    fn foo<'a, 'b>(&'a self, x: &'b str) -> &'a str { }
//                                          ^^^^
//                                          Output gets self's lifetime
}

If a method takes &self or &mut self, the output lifetime matches self.

When Rules Aren't Enough

Sometimes the rules can't figure it out. That's when YOU must add annotations.

Our longer function is an example:

fn longer(s1: &str, s2: &str) -> &str { }

After Rule 1:

fn longer<'a, 'b>(s1: &'a str, s2: &'b str) -> &str { }

Rule 2 doesn't apply (there are TWO input lifetimes, not one).

Rule 3 doesn't apply (no &self).

Rust still doesn't know the output lifetime! So it asks you to specify it.


Step 13: Lifetime Elision in Practice

Let's see some examples of when you do and don't need to annotate.

Example 1: One Input Reference: No Annotation Needed

fn first_char(s: &str) -> &str {
    &s[0..1]
}

Rule 2 applies: one input lifetime → output gets same lifetime.

Rust figures out:

fn first_char<'a>(s: &'a str) -> &'a str {
    &s[0..1]
}

Example 2: No Reference in Output: No Annotation Needed

fn length(s: &str) -> usize {
    s.len()
}

The return type is usize, not a reference. No lifetime needed for the output!

Example 3: Method with &self: No Annotation Needed

struct Document {
    content: String,
}

impl Document {
    fn first_line(&self) -> &str {
        &self.content[0..10]
    }
}

Rule 3 applies: output gets self's lifetime.

Example 4: Two Input References: Annotation NEEDED

fn longer(s1: &str, s2: &str) -> &str {  // ERROR!
    if s1.len() > s2.len() { s1 } else { s2 }
}

Rules can't figure it out. You must write:

fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str {
    if s1.len() > s2.len() { s1 } else { s2 }
}

Step 14: The 'static Lifetime

There's one special lifetime: 'static.

It means: This reference can live for the entire duration of the program.

String Literals Have 'static Lifetime

let s: &'static str = "I live forever!";

Why? String literals are stored directly in the program's binary. They exist from the moment the program starts until it ends.

Breaking It Down

let s: &'static str = "I live forever!";
//     ^^^^^^^
//     This reference is valid for the entire program

When You'll See 'static

1. String literals:

let message: &'static str = "Hello, world!";

2. Error messages sometimes suggest it:

help: consider using the `'static` lifetime

Be careful! When Rust suggests 'static, it's often NOT the right solution. It usually means you need to rethink your design.

When to Actually Use 'static

'static is appropriate when:

'static is NOT appropriate when:


Step 15: Putting It All Together

Let's see a function that uses generics, traits, AND lifetimes!

The Function

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

Breaking It Down Piece by Piece

Lifetime parameter:

fn longest_with_announcement<'a, T>(...)
//                           ^^
//                           Lifetime parameter

Type parameter:

fn longest_with_announcement<'a, T>(...)
//                               ^
//                               Type parameter (generic)

First string parameter:

    x: &'a str,
//     ^^^^^^^
//     Reference to str with lifetime 'a

Second string parameter:

    y: &'a str,
//     ^^^^^^^
//     Also has lifetime 'a

Announcement parameter:

    announcement: T,
//                ^
//                Some generic type T

Return type:

) -> &'a str
//   ^^^^^^^
//   Returns a reference with lifetime 'a

Trait bound:

where
    T: std::fmt::Display,
//     ^^^^^^^^^^^^^^^^^
//     T must implement Display (so we can print it)

Using It

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

fn main() {
    let s1 = String::from("hello");
    let s2 = String::from("world!");
    
    let result = longest_with_announcement(
        &s1,
        &s2,
        "Comparing two strings!",
    );
    
    println!("Longest: {}", result);
}

Output:

Announcement: Comparing two strings!
Longest: world!

Step 16: Common Lifetime Patterns

Here are patterns you'll see often:

Pattern 1: Same Lifetime for Related References

fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str

Use when: The output could come from either input.

Pattern 2: Different Lifetimes

fn first<'a, 'b>(s1: &'a str, s2: &'b str) -> &'a str

Use when: The output only comes from one specific input.

Pattern 3: Struct Holding a Reference

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

Use when: A struct needs to store a reference.

Pattern 4: Methods Returning References

impl<'a> Excerpt<'a> {
    fn text(&self) -> &str {
        self.text
    }
}

Use when: A method returns a reference (elision usually handles this).


Step 17: Quick Reference

Lifetime Annotation Syntax

'a          // A lifetime called 'a
&'a i32     // Reference with lifetime 'a
&'a mut i32 // Mutable reference with lifetime 'a

Function with Lifetimes

fn example<'a>(s: &'a str) -> &'a str { }

Multiple Lifetimes

fn example<'a, 'b>(s1: &'a str, s2: &'b str) -> &'a str { }

Struct with Lifetimes

struct Example<'a> {
    field: &'a str,
}

impl Block with Lifetimes

impl<'a> Example<'a> {
    fn method(&self) -> &str { }
}

Static Lifetime

let s: &'static str = "lives forever";

Step 18: Lifetime Elision Rules Summary

Situation Rule
Each input reference Gets its own lifetime
One input lifetime Output gets that lifetime
Method with &self Output gets self's lifetime

If rules can't determine output lifetime → you must annotate.


Summary: What We Learned About Lifetimes

  1. Lifetimes prevent dangling references: references to data that no longer exists

  2. The borrow checker compares lifetimes: ensures references don't outlive their data

  3. Lifetime annotations describe relationships: they don't change how long things live

  4. Syntax: 'a is a lifetime, &'a T is a reference with that lifetime

  5. Functions: Declare with <'a>, use in parameters and return types

  6. Structs: Need lifetimes when they hold references

  7. Elision rules: Rust often figures out lifetimes automatically

  8. 'static: Special lifetime meaning "lives for entire program"

  9. Same 'a on multiple params: Means "valid for the shorter of these lifetimes"


You Did It!

You've now learned all three major concepts of Chapter 10:

These three concepts work together:

fn longest_with_announcement<'a, T>(
    x: &'a str,          // Lifetime
    y: &'a str,          // Lifetime
    announcement: T,     // Generic
) -> &'a str             // Lifetime
where
    T: Display,          // Trait bound
{ }

This is the foundation for a huge portion of Rust code you'll read and write!


Lifetimes Exercises


Exercise 1: Spot the Problem

This code won't compile. Explain WHY:

fn main() {
    let r;
    {
        let x = 5;
        r = &x;
    }
    println!("{}", r);
}

Hint: Think about when x gets dropped and when r tries to use it.


Exercise 2: Fix It With Scope

Fix Exercise 1 by moving println! inside the inner block:

fn main() {
    {
        let x = 5;
        let r = &x;
        println!("{}", r);
    }
}

Verify it compiles. Explain why this version works.


Exercise 3: Your First Lifetime Annotation

This function won't compile:

fn get_first(s: &str) -> &str {
    &s[0..1]
}

Actually, try it! It DOES compile. Why?

Answer: Rust's elision rules figure it out automatically when there's only ONE input reference.


Exercise 4: When Elision Fails

This function won't compile:

fn longer(s1: &str, s2: &str) -> &str {
    if s1.len() > s2.len() {
        s1
    } else {
        s2
    }
}

Try it. Read the error message.

Question: Why can't Rust figure out the lifetime here?


Exercise 5: Add Lifetime Annotations

Fix Exercise 4 by adding lifetime annotations:

fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str {
    if s1.len() > s2.len() {
        s1
    } else {
        s2
    }
}

Test:

let s1 = String::from("hello");
let s2 = String::from("hi");
let result = longer(&s1, &s2);
println!("Longer: {}", result);

Expected output:

Longer: hello

Exercise 6: Understanding the Annotation

Using longer from Exercise 5, this code works:

fn main() {
    let s1 = String::from("hello");
    let s2 = String::from("world");
    let result = longer(&s1, &s2);
    println!("{}", result);
}

But this code FAILS:

fn main() {
    let s1 = String::from("hello");
    let result;
    {
        let s2 = String::from("world");
        result = longer(&s1, &s2);
    }
    println!("{}", result);
}

Explain why the second version fails.


Exercise 7: Struct with Lifetime

This won't compile:

struct Holder {
    text: &str,
}

Fix it by adding a lifetime:

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

Test:

let s = String::from("hello");
let h = Holder { text: &s };
println!("{}", h.text);

Expected output:

hello

Exercise 8: Struct Lifetime Limits

Using Holder from Exercise 7, this fails:

fn main() {
    let h;
    {
        let s = String::from("hello");
        h = Holder { text: &s };
    }
    println!("{}", h.text);
}

Explain why. Then fix it by moving things around.


Exercise 9: Method on Struct with Lifetime

Add a method to Holder:

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

impl<'a> Holder<'a> {
    fn get(&self) -> &str {
        self.text
    }
}

Test:

let s = String::from("hello");
let h = Holder { text: &s };
println!("{}", h.get());

Expected output:

hello

Exercise 10: The 'static Lifetime

String literals have 'static lifetime, they live forever.

let s: &'static str = "I live forever!";

Create a function that returns a 'static string:

fn get_greeting() -> &'static str {
    "Hello, world!"
}

Test:

let greeting = get_greeting();
println!("{}", greeting);

Expected output:

Hello, world!

Question: Why can string literals be 'static?