When a struct needs to borrow
You are building a text processing tool. You load a massive document into memory. You want to create a Highlight struct that points to a specific phrase within that document. Copying the phrase into the struct wastes memory and slows things down. You want the struct to hold a reference to the original text.
You write the struct with a &str field. The compiler rejects the code. It doesn't know how long the Highlight will live compared to the document. The struct could outlive the document, leaving a reference to freed memory. Rust refuses to guess. You have to tell the compiler the relationship between the struct's lifetime and the reference's lifetime.
That relationship is a lifetime annotation. It turns a vague reference into a precise contract. The struct says, "I hold a reference that lives for duration 'a. I promise I will not exist longer than 'a."
The concept: a lease on data
Think of a struct with a reference like a library card attached to a book. The card points to the book. The card is useless if the book gets destroyed. The lifetime annotation is the rule that says the card must be destroyed before the book is.
In Rust, every reference has a lifetime. That lifetime is the scope where the reference is valid. When a struct holds a reference, the struct inherits that constraint. The struct cannot outlive the data it points to.
The syntax <'a> introduces a lifetime parameter. It works like a generic type parameter T, but instead of naming a type, it names a duration. You can name it anything, but 'a is the convention for the first lifetime. The compiler uses this name to track scopes. If a value has lifetime 'a, the compiler ensures that value stays alive for at least as long as 'a requires.
Minimal example
Here is a struct that holds a reference to a string slice. The lifetime parameter ties the struct to the reference.
struct Highlight<'a> {
// 'a is the lifetime parameter. It names the duration the reference must be valid.
// The struct cannot outlive 'a because it holds borrowed data from that duration.
text: &'a str,
}
fn main() {
// `document` owns the String. It lives for the duration of main().
let document = String::from("Rust prevents dangling references.");
// `Highlight` borrows from `document`.
// The compiler infers that 'a is the lifetime of `document`.
// `h` cannot outlive `document`.
let h = Highlight { text: &document };
// This works. `h` and `document` are both alive.
println!("Highlight: {}", h.text);
}
The struct definition struct Highlight<'a> declares that Highlight is generic over a lifetime 'a. The field text: &'a str uses that lifetime. When you create an instance, the compiler picks a concrete lifetime for 'a based on the data you pass. In this case, 'a becomes the lifetime of document. The compiler guarantees h is dropped before document.
Structs do not get elision
Functions in Rust can sometimes skip lifetime annotations thanks to elision rules. The compiler can infer lifetimes in simple function signatures. Structs do not get this pass.
If a struct contains a reference, it must have an explicit lifetime parameter. There is no elision for struct definitions. This rule keeps the API clear. Anyone reading the struct definition sees immediately that the struct borrows data and is tied to a specific lifetime.
Functions get a pass. Structs do not. If there's a reference, there's a lifetime.
Realistic example: creating and using the struct
In practice, you often create structs inside functions and return them. The lifetime annotation connects the input data to the output struct.
struct Highlight<'a> {
text: &'a str,
score: u32,
}
// The lifetime 'a connects the input slice to the output struct.
// The function promises: the returned Highlight lives no longer than the input slice.
fn create_highlight<'a>(input: &'a str, score: u32) -> Highlight<'a> {
Highlight { text: input, score }
}
fn main() {
let doc = String::from("Safety without sacrifice.");
// `create_highlight` borrows from `doc`.
// The returned `Highlight` is tied to `doc`'s lifetime.
let h = create_highlight(&doc, 99);
println!("Score: {}", h.score);
}
The function signature fn create_highlight<'a>(input: &'a str) -> Highlight<'a> is a contract. The input reference has lifetime 'a. The output struct has lifetime 'a. The compiler enforces that the output cannot outlive the input. If you tried to return a Highlight referencing a local variable inside the function, the compiler would reject it.
Multiple lifetime parameters
A struct might hold references from different sources. Those sources might have different lifetimes. Using a single lifetime parameter ties all references together. That can be too restrictive.
If the references are independent, use multiple lifetime parameters. This decouples the constraints and makes the struct more flexible.
struct Comparison<'a, 'b> {
// 'a and 'b are independent lifetimes.
// `first` and `second` can come from different scopes.
first: &'a str,
second: &'b str,
}
fn main() {
let long_lived = String::from("Permanent data");
{
let short_lived = String::from("Temporary data");
// `Comparison` uses two lifetimes.
// 'a is tied to `long_lived`.
// 'b is tied to `short_lived`.
let comp = Comparison {
first: &long_lived,
second: &short_lived,
};
// This works. Both references are valid inside this scope.
println!("{:?} vs {:?}", comp.first, comp.second);
}
// `short_lived` is dropped here. `comp` is dropped here.
// `long_lived` continues to live.
}
If you used a single 'a for both fields, the compiler would require both references to live for the same duration. That forces long_lived to be treated as short-lived, or prevents the struct from being created. Multiple lifetimes give the compiler the freedom to track each reference independently.
Naming convention: use 'a and 'b for the first two lifetimes. If the names don't convey meaning, stick to 'a and 'b. Descriptive names like 'input and 'output are allowed but rare in structs. The community prefers short names to keep signatures readable.
Implementing methods
When you implement methods for a struct with lifetimes, the impl block must repeat the lifetime parameter. The impl block is generic over the lifetime, just like the struct.
impl<'a> Highlight<'a> {
// The method borrows `self`.
// The lifetime of `self` is tied to 'a.
fn show(&self) {
println!("Highlight: {}", self.text);
}
// This method returns a reference tied to 'a.
// It extracts the inner reference without changing lifetimes.
fn get_text(&self) -> &'a str {
self.text
}
}
The impl<'a> syntax declares that the implementation applies to any lifetime 'a. The methods can use 'a in their signatures. If a method returns a reference from the struct, that reference inherits the struct's lifetime.
The impl block mirrors the struct. If the struct has a lifetime, the impl needs it too.
Pitfalls and compiler errors
Lifetime annotations make constraints explicit. That clarity catches bugs at compile time. Here are common mistakes and the errors they trigger.
Returning a reference to local data
You cannot return a struct that references data created inside the function. The local data is dropped when the function returns. The struct would hold a dangling reference.
struct Highlight<'a> {
text: &'a str,
}
fn bad_highlight() -> Highlight<'static> {
let doc = String::from("Local data");
// E0515: cannot return value referencing local variable `doc`
// `doc` is dropped at the end of this function.
// The returned struct would reference freed memory.
Highlight { text: &doc }
}
The compiler rejects this with E0515 (cannot return value referencing local variable). The lifetime annotation makes the problem obvious. The struct claims to hold a reference with lifetime 'static (or any lifetime), but the data only lives inside the function. The contract is broken.
Fix this by returning owned data instead. Change the struct to hold a String, or return the String alongside the struct.
Borrowed value does not live long enough
You might try to use a struct after the data it references has been dropped. The compiler tracks scopes and enforces the lifetime.
struct Highlight<'a> {
text: &'a str,
}
fn main() {
let h;
{
let doc = String::from("Short scope");
// `h` borrows from `doc`.
// `h`'s lifetime is tied to `doc`'s scope.
h = Highlight { text: &doc };
}
// `doc` is dropped here.
// E0597: `doc` does not live long enough
// `h` tries to use `doc` after it's gone.
println!("{}", h.text);
}
The compiler rejects this with E0597 (borrowed value does not live long enough). The lifetime annotation ensures h cannot be used after doc is dropped. If you need the data to persist, move the data out of the scope or use owned types.
The compiler isn't being difficult. It's saving you from a dangling pointer that would crash your program at runtime.
Decision: when to use lifetimes in structs
Structs with lifetimes are a powerful tool, but they add complexity. Choose the right pattern for your situation.
Use lifetime annotations on struct fields when the struct borrows data and you want to avoid the cost of copying. This is ideal for views, handles, or wrappers around large data that lives elsewhere.
Use owned types like String or Vec<T> when the struct must own the data, or when the data needs to outlive the original source. Ownership removes lifetime constraints and makes the struct self-contained.
Use Cow<str> when the struct might hold either borrowed or owned data depending on runtime conditions. Cow (Clone on Write) lets you start with a reference and clone to owned data only when necessary.
Use multiple lifetime parameters when the struct holds references from independent sources with different scopes. This decouples constraints and increases flexibility.
Reach for &str directly when you only need to pass a slice to a function without bundling it in a custom type. A raw reference is simpler than a struct with a single field.
Treat the lifetime parameter as a contract. If you can't guarantee the data lives long enough, change the design.