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 , to , or to something else. The annotation tells Rust:
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.() >= b.() {
a
} {
b
}
}
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
= ::();
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:
<>(a: & , b: & ) & {
!a.() { a } { b }
}
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()
} {
(input)
}
}
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);
}
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
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
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
Accessing Cargo Package Metadata in Rust
Learn how to read Cargo package metadata like version, name, and authors in Rust using compile-time environment macros.
Default Function Arguments in Rust: What to Use Instead
Learn how Rust handles default function arguments, why they are not supported, and practical patterns to achieve similar behavior.
Fixing Rust "linker 'cc' not found" on Debian in WSL
Learn why Rust shows "linker 'cc' not found" on Debian in WSL and how to fix it by installing the required C build tools.