Lifetimes in Rust
December 22, 2025In 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:
- Generics - Writing flexible, reusable code with type parameters
- Traits - Defining shared behavior across different types
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.
- The book exists on the library shelf (the data)
- You have a library card that lets you borrow it (the reference)
- The book has a due date (the lifetime)
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:
- Data exists somewhere (owned by a variable)
- References let you "borrow" that data
- The lifetime is how long the borrow is valid
- When the data goes away, all references to it become invalid
Another Analogy: Apartment Leases
Think of it like renting an apartment:
- The apartment is the data
- Your lease is the reference
- The lease duration is the lifetime
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:
- How long the DATA lives
- 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:
xlives for the whole functionrlives for the whole functionrnever outlivesx✓
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:
xonly lives inside the inner blockrlives for the whole functionroutlivesx✗
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:
xdoesn't live long enoughxis dropped while still borrowed- You tried to use the borrow after
xwas dropped
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:
s1lives for some duration'as2lives for some duration'a- The return value lives for duration
'a 'ais the SHORTER of the two input lifetimes
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:
string1lives until end ofmainstring2lives until end ofmainresultlives until end ofmain- Everyone lives long enough ✓
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:
longerreturns something with lifetime'a'ais the shorter of the two input lifetimesstring2has a shorter lifetime (inner block only)- So
resultmight only be valid for the inner block - But you try to use
resultafter the inner block - 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:
novelowns the string datafirst_sentenceis a reference to part ofnovelexcerpt.textstores that reference- Everything lives until the end of
main excerptdoesn't outlivenovel✓
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:
- You're working with string literals
- You truly have data that lives forever (global constants, etc.)
'static is NOT appropriate when:
- You're just trying to make the compiler happy
- You could restructure your code instead
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
Lifetimes prevent dangling references: references to data that no longer exists
The borrow checker compares lifetimes: ensures references don't outlive their data
Lifetime annotations describe relationships: they don't change how long things live
Syntax:
'ais a lifetime,&'a Tis a reference with that lifetimeFunctions: Declare with
<'a>, use in parameters and return typesStructs: Need lifetimes when they hold references
Elision rules: Rust often figures out lifetimes automatically
'static: Special lifetime meaning "lives for entire program"Same
'aon multiple params: Means "valid for the shorter of these lifetimes"
You Did It!
You've now learned all three major concepts of Chapter 10:
- Generics: Write code once, use with any type
- Traits: Define capabilities, limit generics to types with those capabilities
- Lifetimes: Ensure references are always valid
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?