Question
I am reading the lifetimes chapter of The Rust Programming Language and came across this example using a named lifetime:
struct Foo<'a> {
x: &'a i32,
}
fn main() {
let x;
{
let y = &5;
let f = Foo { x: y };
x = &f.x; // error here
}
println!("{}", x);
}
I understand the error the compiler is preventing: assigning &f.x to x would allow a reference to outlive the data it ultimately points to, which would create a use-after-free problem.
What I do not understand is why the explicit lifetime 'a is needed in the first place. It seems like the compiler could reject x = &f.x just by analyzing scopes and seeing that the assignment would make a reference escape into a wider scope.
So, in what situations are explicit lifetimes actually necessary in Rust? Are they only for preventing use-after-free, or do they serve a broader purpose in the type system?
Short Answer
By the end of this page, you will understand that explicit lifetimes in Rust are not mainly there because the compiler cannot see simple scope errors. Instead, they are used to describe relationships between references in types and function signatures. Rust can often infer lifetimes inside a function body, but when references cross function boundaries or become part of a struct's type, the compiler needs you to state how long those references are connected.
Concept
Rust lifetimes describe how long references are valid. More importantly, they describe relationships between borrows.
A common beginner misunderstanding is: "Lifetimes are only needed because Rust cannot figure out how long a variable lives." That is not quite right.
Rust is already very good at analyzing local scopes:
fn main() {
let r;
{
let x = 10;
r = &x; // rejected
}
println!("{}", r);
}
This does not need explicit lifetime annotations for Rust to reject it.
So why do explicit lifetimes exist?
Because sometimes Rust must reason not just about one reference, but about how multiple references are related.
For example:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
Here, the return value is a reference. Rust needs to know whether that returned reference is tied to x, to y, or to something else. The annotation 'a tells Rust:
- both input references must be valid for at least
'a - the returned reference is valid for that same
'a
Without that annotation, Rust would know the return value is some borrowed string slice, but not which input it is borrowed from.
That is the key purpose of explicit lifetimes:
- to express borrowing relationships in APIs
- to make reference-containing types well-defined
- to let the borrow checker verify safety across function and type boundaries
They do help prevent use-after-free, but they are part of a broader system: Rust's way of encoding aliasing and borrowing rules into types.
Why structs with references need lifetimes
Consider:
struct Foo<'a> {
x: &'a i32,
}
The lifetime 'a does not mean "find the scope manually." It means:
- any
Foo<'a>contains a reference - that reference must stay valid for at least
'a - code using
Foo<'a>must respect that contract
Without 'a, the type would be incomplete. Rust would not know what kind of borrowing guarantee a Foo value carries.
Why inference is limited
Rust does infer many lifetimes, especially inside function bodies. But type signatures are part of a public contract. If a function returns a reference, the compiler cannot guess the intended relationship in every case.
For example, this is ambiguous without annotations:
fn pick(x: &str, y: &str) -> &str {
x
}
Should the output lifetime be tied to x only? To both? To the shorter one? Rust requires an explicit annotation unless one of the lifetime elision rules applies.
So explicit lifetimes are needed less for simple local analysis, and more for expressing how borrowed data flows through types and functions.
Mental Model
Think of a lifetime like a label on a borrowed library book.
- The book belongs to someone else.
- You are only allowed to hold it for a certain period.
- If you hand that book to someone else, they need to know how long they are allowed to keep it.
Now imagine a struct or function as a delivery service.
- A struct with a reference is like a box containing a borrowed book.
- A function returning a reference is like forwarding a borrowed book to someone else.
Rust needs labels saying where the book came from and how long that permission lasts.
Inside a small room, Rust can often see everything directly and infer the timing. But when a book is passed through a function or stored in a type, Rust needs a written rule.
That written rule is the lifetime annotation.
Syntax and Examples
Basic struct syntax with a lifetime
struct Foo<'a> {
x: &'a i32,
}
This means Foo contains a reference to an i32, and that reference must be valid for lifetime 'a.
Basic function syntax with lifetimes
fn identity<'a>(value: &'a str) -> &'a str {
value
}
This says the returned &str lives as long as the input value reference.
Example: why the relationship matters
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() {
a
} else {
b
}
}
Explanation
aandbare both borrowed string slices.- The function returns one of them.
'atells Rust that the output cannot outlive either input borrow.
Example where annotations are not needed
Rust has lifetime elision rules for common cases:
fn first_word(s: &str) -> &str {
s.split_whitespace().next().unwrap_or("")
}
Rust can infer the lifetime here because there is only one input reference, so the output is assumed to be tied to it.
Example with a struct method
struct Message<'a> {
text: &'a str,
}
impl<'a> Message<'a> {
fn get_text(&self) -> &str {
self.text
}
}
Here:
Message<'a>stores borrowed textget_textreturns a reference to that text- method lifetime elision usually makes the method signature simpler
Step by Step Execution
Consider this example:
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() > b.len() {
a
} else {
b
}
}
fn main() {
let s1 = String::from("apple");
let result;
{
let s2 = String::from("pear");
result = longest(s1.as_str(), s2.as_str());
println!("Inside block: {}", result);
}
// println!("Outside block: {}", result); // would fail
}
Step by step
1. s1 is created
let s1 = String::from("apple");
s1 lives until the end of main.
2. result is declared
let result;
No value yet.
3. s2 is created inside the inner block
let s2 = String::from("pear");
s2 only lives until the block ends.
4. Call longest
result = longest(s1.as_str(), s2.as_str());
The function may return either s1.as_str() or s2.as_str().
So Rust must assume the returned reference is only safe for the shorter of the two input lifetimes.
That means result is only valid until s2 goes out of scope.
5. Printing inside the block is fine
println!("Inside block: {}", result);
At this point, both s1 and s2 are still alive.
6. After the block, s2 is dropped
If Rust allowed result to be used after this point, it might point to s2, which is gone. So Rust rejects that use.
Key takeaway
The lifetime annotation on longest does not manually control memory. It tells Rust how the return value is connected to the input references, so Rust can reject unsafe uses afterward.
Real World Use Cases
Lifetimes show up whenever you borrow data instead of owning it.
1. Returning slices from strings
fn prefix(s: &str) -> &str {
&s[..3.min(s.len())]
}
This returns a borrowed part of the input string, not a new allocation.
2. Structs that borrow configuration or input data
struct RequestView<'a> {
path: &'a str,
method: &'a str,
}
Useful when parsing HTTP requests without copying every field.
3. Parsers and tokenizers
Many parsers return slices into the original input instead of allocating new strings.
struct Token<'a> {
lexeme: &'a str,
}
4. API helpers that return references
A function may return a reference to data passed in:
fn non_empty<'a>(a: &'a str, b: &'a str) -> &'a str {
if !a.is_empty() { a } else { b }
}
5. Avoiding unnecessary cloning
Borrowing with lifetimes lets you write efficient code without copying data everywhere.
That is one of the main reasons lifetimes matter in real programs.
Real Codebase Usage
In real Rust codebases, developers often use lifetimes in a few recurring patterns.
Borrowed views over owned data
A large application may own a String, but pass around &str views to avoid allocations.
fn log_message(msg: &str) {
println!("LOG: {}", msg);
}
Structs that reference external data
This is common in parsing, configuration loading, and zero-copy processing.
struct UserRow<'a> {
name: &'a str,
email: &'a str,
}
Validation functions returning borrowed data
fn validated_name(input: &str) -> Result<&str, &'static str> {
if input.trim().is_empty() {
Err("name cannot be empty")
} else {
Ok(input)
}
}
Guard clauses with borrowed inputs
Rust developers commonly validate early and then return references safely.
fn first_char(s: &str) -> Option<&str> {
if s.is_empty() {
return None;
}
Some(&s[..1])
}
Methods that tie output to self
struct Document {
text: String,
}
impl Document {
fn as_str(&self) -> &str {
&self.text
}
}
Here, lifetime elision hides the details, but the returned &str is still tied to &self.
When developers avoid explicit lifetimes
If ownership is simpler, developers often return owned values instead:
fn longest_owned(a: &str, b: &str) -> String {
if a.len() > b.len() {
a.to_string()
} else {
b.to_string()
}
}
This avoids lifetime complexity at the cost of allocation.
Common Mistakes
1. Thinking lifetimes create or extend data lifetimes
They do not.
Lifetimes only describe borrowing constraints. They do not keep values alive longer.
Incorrect idea
fn bad<'a>() -> &'a str {
let s = String::from("hello");
&s
}
This fails because s is dropped when the function ends.
2. Thinking explicit lifetimes are needed everywhere
They are not.
Rust often infers them using lifetime elision rules.
fn len(s: &str) -> usize {
s.len()
}
No explicit annotation needed.
3. Confusing ownership with borrowing
fn print_name(name: String) {
println!("{}", name);
}
This takes ownership.
fn print_name(name: &str) {
println!("{}", name);
}
This borrows.
Lifetimes matter for the second form.
4. Returning a reference when you should return an owned value
Broken approach:
fn make_message<'a>() -> &'a str {
let msg = String::from("hi");
&msg
}
Correct approach:
fn make_message() -> String {
String::from("hi")
}
5. Not realizing a returned reference may be tied to the shortest input lifetime
fn choose<'a>(a: &'a str, b: &'a str) -> &'a str {
a
}
Even if the function body always returns a, the shared 'a means both inputs are constrained together.
If you want the output tied only to a, use separate lifetimes:
fn choose_first<'a, 'b>(a: &'a str, _b: &'b str) -> &'a str {
a
}
6. Adding lifetimes to owned types unnecessarily
struct Person<'a> {
name: String,
marker: &'a str,
}
If the struct only needs owned data, do not add reference fields and lifetimes unless necessary.
Comparisons
| Concept | What it means | When needed | Example |
|---|---|---|---|
| Lifetime inference | Rust figures out the relationship automatically | Simple local code and common function forms | fn first(s: &str) -> &str |
| Explicit lifetimes | You describe how references are related | Ambiguous function signatures, structs with references | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str |
| Owned return value | Return new owned data instead of borrowing | When borrowing is too restrictive or impossible | fn build() -> String |
| Borrowed return value | Return a reference to existing data | When output comes from input and no allocation is needed | fn head(s: &str) -> &str |
Explicit lifetimes vs scope checking
| Situation | Can Rust usually infer it? | Need explicit lifetime? |
|---|---|---|
| Local variable borrow inside one function | Yes | Usually no |
| Reference stored in a struct | No, must be part of the type | Yes |
| Function returning one borrowed input | Sometimes, via elision | Often no |
| Function returning one of several borrowed inputs | Not always | Yes |
| Returning owned data | No lifetime issue on return | No |
Lifetimes vs ownership
| Feature | Ownership | Lifetimes |
|---|---|---|
| Purpose | Controls who owns and drops data | Describes how long borrows are valid |
| Drops memory? | Yes | No |
| Prevents dangling references? | Indirectly | Yes, directly through borrow rules |
| Used with references? | Sometimes | Always relevant for references |
Cheat Sheet
Quick rules
- Lifetimes describe borrow relationships, not how long memory is manually kept alive.
- Rust often infers lifetimes in local code.
- You usually write explicit lifetimes when:
- a struct contains references
- a function returns a reference tied to inputs
- multiple input references make the return relationship ambiguous
Common syntax
struct Foo<'a> {
x: &'a i32,
}
fn identity<'a>(x: &'a str) -> &'a str {
x
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
Lifetime elision shortcuts
Rust can often infer lifetimes when:
- there is exactly one input reference
- or in methods where output is tied to
&self
Examples:
fn first(s: &str) -> &str
fn name(&self) -> &str
Important idea
This:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str
means:
- output is valid for at most
'a - both inputs must be valid for at least
'a - in practice, the returned reference cannot outlive the shorter relevant borrow
When to return owned data instead
Use owned data like String when:
- the result is created inside the function
- borrowing would return a dangling reference
- API simplicity matters more than avoiding allocation
Red flags
These usually mean a lifetime problem:
- returning
&strfrom a function that creates aString - storing references in structs without lifetime parameters
- trying to use a borrowed value after its source goes out of scope
FAQ
Why does Rust need explicit lifetimes if it already knows scopes?
Because lifetimes are not only about local scopes. They also describe how references in function signatures and struct types are related.
Are lifetimes only for preventing use-after-free?
Mostly they help prevent dangling references, but they also express borrowing contracts in the type system.
Why does a struct with references need a lifetime parameter?
Because the struct's type must record how long its borrowed fields are valid.
When can Rust infer lifetimes automatically?
Usually in simple functions covered by lifetime elision rules and inside function bodies where the compiler sees all scopes.
Do lifetime annotations extend how long a value lives?
No. They only describe constraints on references. They never keep data alive longer.
Why does String often avoid lifetime issues?
Because String owns its data. Owned values do not depend on some external lifetime the way references do.
Should I use owned values instead of lifetimes?
Sometimes yes. If borrowing makes the API hard to use, returning or storing owned data can be simpler.
What is the most important thing to remember about lifetimes?
They describe which borrowed data a reference depends on and ensure that reference is never used after the source becomes invalid.
Mini Project
Description
Build a small Rust program that returns borrowed string slices from input data. This demonstrates when explicit lifetimes are needed in function signatures and how borrowed outputs depend on borrowed inputs.
Goal
Create a program that safely picks and returns the longer of two string slices without allocating a new String.
Requirements
- Define a function that accepts two
&strvalues. - Return one of the input string slices instead of creating a new string.
- Use an explicit lifetime annotation in the function signature.
- Call the function from
mainwith strings of different scopes. - Show a valid use inside scope and avoid invalid use outside scope.
Keep learning
Related questions
Absolute Values in Rust: Using the abs() Method
Learn how to calculate absolute values in modern Rust with abs(), handle signed integer overflow, and choose safer alternatives.
Accepting Iterators in Rust Functions with impl Iterator
Learn how to write Rust functions that accept HashMap iterators using generic bounds or impl Iterator, avoiding trait object and lifetime errors.
Access Enum Variant Values in Rust with Pattern Matching
Learn how to extract and print values stored in Rust enum variants using match and if let pattern matching, with practical examples.