The apostrophe that saves your memory
You write a function to find the longer of two string slices. You pass in &str references. The compiler rejects you with a wall of text about lifetimes. You look at the error message and see 'a floating around in examples and suggestions. It looks like a typo. It looks like a variable name with a weird prefix.
It is neither. 'a is Rust's syntax for a lifetime parameter. It is a label that names a duration of time. Rust uses it to prove that a reference never outlives the data it points to. Without 'a, the compiler cannot verify that your pointers remain valid, so it refuses to compile.
What 'a actually means
Rust's ownership system guarantees memory safety without a garbage collector. The core rule is simple: every value has one owner, and when the owner goes out of scope, the value is dropped. References let you borrow a value without taking ownership. The danger is dangling references: pointers to memory that has already been freed.
'a is the tool Rust uses to track borrowing durations. It is not a variable that holds data. It is a variable that holds a time span. When you see 'a in code, read it as "this reference must be valid for at least the duration labeled 'a."
Think of a library book. The book is the data. A reference is a library card that lets you check out the book. 'a is the due date clause on the card. It doesn't create the book. It doesn't extend the book's existence. It just records the rule: "You can hold this card only while the book is available." If the book is destroyed, the card becomes invalid immediately, regardless of what the clause says.
Lifetimes do not change how long values live. They only describe relationships between how long references live and how long the data lives. The compiler uses these descriptions to catch errors before your program runs.
Minimal example: connecting references
Here is the longest function that returns the longer of two string slices. The signature uses 'a to tie the input references to the output reference.
/// Returns the longer of two string slices.
/// The lifetime 'a ensures the return value is valid
/// as long as both inputs are valid.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
// 'a is a lifetime parameter. It names the scope
// that all references in this signature must cover.
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let string1 = String::from("long string");
// string2 is created here. Its scope starts now.
{
let string2 = String::from("xyz");
// We call longest with references to both strings.
// The compiler infers that 'a must be at least as long
// as the shorter of the two scopes.
let result = longest(&string1, &string2);
println!("The longest string is '{}'", result);
// result is used here. It must be valid.
}
// string2 is dropped here. Its scope ends.
// result can no longer be used if it pointed to string2.
}
The syntax <'a> declares a lifetime parameter named 'a. The annotations &'a str attach that parameter to specific references. The compiler reads this signature as a contract: "The returned reference lives at least as long as 'a. Both x and y live at least as long as 'a. Therefore, the return value lives at least as long as both inputs."
This constraint forces the compiler to check scopes. If you try to return a reference to string2 after string2 is dropped, the constraint fails. The compiler knows the return value would be dangling, so it rejects the code.
Treat 'a as a contract. If you can't prove the reference lives long enough, the compiler won't let you lie.
How the compiler checks lifetimes
The compiler does not track exact execution times. It tracks scopes. When it sees a function with lifetime parameters, it builds a set of constraints.
- It identifies the scope of each variable.
- It substitutes the lifetime parameter with the smallest scope that satisfies all references using that parameter.
- It verifies that the substituted scope is valid for every usage.
In the longest example, string1 lives for the entire main function. string2 lives only inside the inner block. When you call longest(&string1, &string2), the compiler sees that x has scope main and y has scope inner. The parameter 'a is shared by both. The compiler infers 'a must be the intersection of the two scopes. The intersection is inner. The return value is labeled 'a, so the return value is valid only inside inner. If you try to use result after the inner block closes, the compiler rejects you with E0597 (borrowed value does not live long enough).
This inference happens automatically. You rarely need to write 'a explicitly in function calls. You write it in signatures to tell the compiler how references relate to each other.
Realistic example: structs with references
Structs often hold references. When a struct contains a reference, the struct itself must carry a lifetime parameter. The struct cannot outlive the data it references.
/// A quote that borrows text from an external source.
/// The lifetime 'a ties the struct to the borrowed text.
struct Quote<'a> {
// text borrows a string slice. It must live at least as long as 'a.
text: &'a str,
// author is owned. It lives as long as the Quote itself.
author: String,
}
fn main() {
let source = String::from("To be or not to be");
// Create a Quote that borrows from source.
// The compiler infers the lifetime of Quote matches
// the lifetime of the reference to source.
let quote = Quote {
text: &source,
author: String::from("Shakespeare"),
};
println!("Quote: {}", quote.text);
// source is dropped here.
// quote is dropped here too, so this is safe.
}
The Quote struct has a lifetime parameter 'a. The field text uses &'a str. This means a Quote<'a> can only exist while the borrowed text is valid for duration 'a. If you tried to store a Quote in a global variable while source is a local variable, the compiler would reject it. The global Quote would need a lifetime of 'static, but source dies at the end of main. The constraints clash.
Convention aside: When a struct has only one lifetime parameter, developers often name it 'a. If there are multiple, names like 'a and 'b help distinguish them. If you don't care about the name, you can use '_ as a placeholder. '_ tells the compiler "infer this lifetime, but don't expose it in the public API." You see '_ often in method signatures or internal helpers.
Trust the borrow checker. It usually has a point when lifetimes clash.
Lifetime elision: when you don't need 'a
You might notice that many functions work without explicit lifetime annotations. Rust has lifetime elision rules that let the compiler infer lifetimes in common patterns. You only need to write 'a when the pattern is ambiguous.
The elision rules are:
- Each input reference gets its own lifetime parameter.
- If there is exactly one input lifetime, that lifetime is assigned to all output lifetimes.
- If there are multiple input lifetimes but one of them is
&selfor&mut self, the lifetime ofselfis assigned to all output lifetimes.
These rules cover most simple functions.
/// This function compiles without explicit lifetimes.
/// Elision rule 2 applies: one input lifetime, so output gets it.
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
/// This function also compiles without explicit lifetimes.
/// Elision rule 3 applies: &self provides the output lifetime.
struct TextBuffer {
content: String,
}
impl TextBuffer {
fn get_content(&self) -> &str {
&self.content
}
}
In first_word, there is one input reference s. The compiler assigns it a lifetime (say 'a) and gives the return value the same lifetime. In get_content, the receiver is &self. The compiler assigns self's lifetime to the return value.
Elision saves typing. It also keeps signatures clean. You only add 'a when the compiler cannot guess. This happens when you have multiple input references and the return value could come from any of them, or when you have no input references but return a reference (which is usually an error anyway).
Trust the elision rules. They cover 90% of your code. Add 'a only when the compiler forces you to disambiguate.
Pitfalls and compiler errors
Lifetimes trip up beginners in predictable ways. Recognizing the error codes helps you fix the problem faster.
Missing lifetime specifier. If you write a function that returns a reference but forget the lifetime parameter, the compiler rejects you with E0106 (missing lifetime specifier).
// Error E0106: missing lifetime specifier
fn longest_bad(x: &str, y: &str) -> &str {
if x.len() > y.len() { x } else { y }
}
The compiler doesn't know how the output relates to the inputs. It could be x, it could be y, or it could be a static string. You must add 'a to clarify.
Borrowed value does not live long enough. If you return a reference to a local variable, the compiler rejects you with E0515 (cannot return reference to local variable) or E0597 (borrowed value does not live long enough).
// Error E0515: cannot return reference to local variable
fn create_string() -> &str {
let s = String::from("hello");
&s
}
s is dropped at the end of the function. Returning &s would create a dangling reference. Adding 'a won't fix this. The data must outlive the function. You need to return an owned String instead, or pass the data in from outside.
Mismatched lifetimes in structs. If a struct holds references with different lifetimes, you need multiple lifetime parameters. Using one parameter for everything forces an unnecessary constraint.
// This struct forces text and title to share the same lifetime.
// That might be too restrictive.
struct BadExcerpt<'a> {
text: &'a str,
title: &'a str,
}
// Better: separate lifetimes if they come from different sources.
struct GoodExcerpt<'a, 'b> {
text: &'a str,
title: &'b str,
}
If text comes from a file and title comes from a database, they might have different scopes. GoodExcerpt allows them to differ. BadExcerpt forces them to share the shorter scope, which can cause E0597 errors when you try to use the struct.
Don't fight the lifetime checker with unsafe. Fix the scope or own the data.
Decision: when to use lifetimes vs alternatives
Rust gives you choices. Pick the tool that matches your data flow.
Use 'a when you need to relate the lifetimes of multiple references in a function signature or struct. Use 'a when you are borrowing data and want to avoid copying. Use 'a when the data is large or immutable and sharing references is efficient.
Use lifetime elision when the pattern is simple. Use elision when you have one input reference or a method with &self. Use elision to keep signatures readable.
Use Rc<T> or Arc<T> when you need shared ownership and cannot express the relationship with lifetimes. Use Rc<T> for single-threaded code where multiple owners need to read the same data. Use Arc<T> for multi-threaded code where references cross thread boundaries.
Use owned types like String or Vec<T> when you want to avoid lifetime headaches entirely. Use owned types when the data is small and copying is cheap. Use owned types when the data needs to outlive the scope where it was created.
Use Cow<T> when you want to accept either borrowed or owned data without forcing the caller to clone. Use Cow<T> in APIs where performance matters and you want to avoid allocation when the input is already borrowed.
Counter-intuitive but true: the more you use unsafe, the harder the rest of your code becomes to reason about. Stick to safe abstractions whenever possible.