Lifetime Annotation Syntax
How to write lifetime annotations in Rust to describe relationships between reference lifetimes in function signatures, structs, and methods
Lifetime annotations are a syntax Rust uses to describe how the lifetimes of different references relate to one another. They do not change how long a reference actually lives—that is determined by the code’s scope. Instead, annotations tell the borrow checker the minimum guarantee needed for the code to be safe: “the returned reference will live at least as long as this input reference,” or “this struct’s field borrows data that must outlive the struct.”
The need for explicit annotations arises when a function returns a reference that could come from one of several inputs, or when a struct holds a borrowed value. Without the annotation, the compiler cannot verify that the return value won’t outlive the data it points to.
The Syntax of Lifetime Annotations
The syntax is compact but precise. A lifetime name starts with an apostrophe ('), followed by a short, lowercase identifier—by convention 'a, 'b, 'c, and so on. The annotation is placed immediately after the & in a reference type, separated by a space.
&i32 // a reference with no explicit lifetime annotation
&'a i32 // an immutable reference with lifetime 'a
&'a mut i32 // a mutable reference with lifetime 'a
In a generic context—function signatures, struct definitions, or impl blocks—the lifetime parameter is introduced inside angle brackets, exactly like a generic type parameter.
fn example<'a>(x: &'a str) -> &'a str {
x
}
Here 'a is declared as a lifetime parameter, and both x and the return type are annotated with it. The compiler now knows that the returned reference borrows from x and must not outlive it.
Naming Convention:
While 'a is the most common name, any valid Rust identifier works (e.g., 'input, 'ctx). Short names are used because annotations often appear many times in a single signature and long names reduce readability. Reserve descriptive names for situations where multiple distinct lifetimes serve clearly different roles.
What Annotations Do Not Do
A lifetime annotation never causes a value to live any longer than its scope would allow. The annotation is a constraint that the borrow checker enforces; it does not insert code to extend a variable’s lifetime. If the actual lifetimes do not satisfy the constraint, the program fails to compile.
Think of the annotation as a label placed on a relationship. When you write fn f<'a>(x: &'a str, y: &'a str) -> &'a str, you are saying: “There is some intersection of the lifetimes of x and y, and the return reference is valid only within that intersection.” The concrete lifetime 'a will be the shorter of the two input lifetimes at the call site. The annotation does not pick which input is shorter; it just expresses that the result can only be used while both are alive.
Using Lifetime Annotations in Functions
Consider a function that returns the longer of two string slices. The natural first attempt without lifetimes looks like this:
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
The compiler rejects this with an error:
Compile Error E0106:
The signature does not specify whether the returned reference borrows from x or from y. The borrow checker needs to know this relationship to prevent dangling references.
The fix adds a lifetime parameter that links the inputs and the output:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
Now the contract is clear: the returned reference lives at most as long as the shorter of x and y. This means the caller cannot use the result after either input has been dropped.
A complete example that compiles:
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(&string1, &string2);
println!("The longest string is {}", result);
}
The annotation does not require both inputs to have identical concrete lifetimes; it only demands that the overlap between them is large enough to cover every use of the returned reference. If the overlap is too small, the compiler catches it:
fn main() {
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
result = longest(&string1, &string2);
}
// println!("{}", result); // compiler error: `string2` does not live long enough
}
Here string2 goes out of scope before result is used, so the borrow checker refuses even though string1 still lives. The annotation says the return lives as long as the intersection of both inputs—and that intersection ends when string2 is dropped.
Compiles Correctly:
Once the annotations correctly capture the relationship, the function compiles and works exactly like you would expect from the logic inside it.
Multiple Lifetime Parameters
Sometimes the return value depends on only one of the inputs. In that case, using a single lifetime for all inputs would unnecessarily restrict the caller: the return would be tied to the shortest of all input lifetimes, even though it only needs one of them to be alive.
You can declare distinct lifetime parameters to express which input the return borrows from:
fn first<'a, 'b>(x: &'a str, _y: &'b str) -> &'a str {
x
}
Here 'a is the lifetime of x and the return, while 'b is the independent lifetime of y. The caller can pass a y that lives for a shorter time than the return value; only x must remain valid while the result is used.
fn main() {
let string1 = String::from("important data");
let result;
{
let string2 = String::from("temporary");
result = first(&string1, &string2);
}
// `string2` is dropped, but `result` still borrows from `string1`
println!("{}", result); // works fine
}
This decoupling is essential in any API where a function receives data for secondary purposes (e.g., logging, formatting) while the primary return value depends on a single reference.
Lifetime Annotations in Struct Definitions
Structs that hold references must carry lifetime annotations. The annotation ensures that the struct instance cannot outlive the borrowed data it contains.
struct Excerpt<'a> {
part: &'a str,
}
The lifetime parameter 'a is part of the struct’s type, and any instance of Excerpt<'a> is valid only as long as the referenced str is alive.
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().unwrap();
let excerpt = Excerpt { part: first_sentence };
println!("{}", excerpt.part);
// Both `novel` and `excerpt` go out of scope here; everything is safe.
}
If you try to use the struct after the borrowed data is gone, the compiler stops you:
fn main() {
let excerpt;
{
let novel = String::from("Call me Ishmael. Some years ago...");
excerpt = Excerpt { part: &novel };
}
// println!("{}", excerpt.part); // error: `novel` does not live long enough
}
The mental model: a struct with a lifetime parameter is like a receipt for a loan. You cannot use the receipt after the item has been returned.
Structs With Multiple Fields:
If a struct holds several references that may have different lifetimes, you can use multiple lifetime parameters (e.g., struct Pair<'a, 'b> { ... }). But in most cases all fields borrow from the same owner and a single parameter suffices.
Lifetime Annotations in Method Definitions
Methods that borrow self often benefit from lifetime elision, but the underlying annotations are still present in the type system. When you write an impl block for a struct with lifetimes, you must declare the lifetime parameters on the impl block.
impl<'a> Excerpt<'a> {
fn get_part(&self) -> &str {
self.part
}
}
Even though get_part does not contain an explicit lifetime annotation, the compiler infers that the return type borrows from &self, so the returned &str has the same lifetime 'a as the struct. This is lifetime elision rule 3 (methods that take &self or &mut self get the lifetime of self assigned to output references).
When a method takes additional references, you might need explicit annotations if the return value’s lifetime is not tied solely to self. For example, if you write a method that compares an external slice with the struct’s data and returns the longer one, you would need to annotate just like a free function.
impl<'a> Excerpt<'a> {
fn longest_with(&self, other: &str) -> &str {
if self.part.len() > other.len() {
self.part
} else {
other
}
}
}
This code does not compile because the return lifetime could come from self or from other. The fix requires explicit annotations:
impl<'a> Excerpt<'a> {
fn longest_with(&'a self, other: &'a str) -> &'a str {
if self.part.len() > other.len() {
self.part
} else {
other
}
}
}
Now both self and other are tied to the same lifetime 'a, and the return is valid only while both are alive.
Common Misconceptions and Pitfalls
Several patterns repeatedly cause trouble when learning lifetime annotations.
Returning a Reference to a Local Variable:
fn dangling<'a>() -> &'a str {
let s = String::from("hello");
&s // error: `s` does not live long enough
}
The annotation 'a cannot keep a local variable alive. The string s is dropped when the function returns, so any reference to it is invalid. The compiler catches this regardless of what lifetime you annotate. The fix is to return owned data (String) instead.
Overusing 'static:
Seeing 'static in an error message does not mean you should add it everywhere. A 'static lifetime means the reference is valid for the entire program—string literals and constants naturally have it. Slapping 'static on a function signature often hides the real problem and makes the API needlessly restrictive. Use 'static only when the data genuinely lives forever.
Annotations Have No Runtime Effect:
Lifetime annotations are a purely compile-time concept. They are erased before the final binary is generated, so there is zero runtime cost. They exist only to prove memory safety to the borrow checker.
Another common mistake is thinking that 'a extends the lifetime of a reference. It does not. The annotation just makes a promise; if the actual scope does not meet the promise, the code fails to compile. The programmer’s job is to arrange data so that the scopes naturally satisfy the constraints.
Real-World Examples
Explicit lifetime annotations appear throughout the Rust standard library. Recognizing them helps you read and write idiomatic Rust.
std::str::from_utf8: returns aResult<&str, Utf8Error>. The successful&strborrows from the input byte slice, so the function signature uses a lifetime:pub fn from_utf8(v: &[u8]) -> Result<&str, Utf8Error>— the elision rules infer the output lifetime from the single input reference.split_aton slices:pub fn split_at(&self, mid: usize) -> (&[T], &[T]). Both returned slices borrow fromself; the compiler infers that they share the lifetime of&self.- Custom iterators: Many iterator adapters that hold a reference to the original collection carry a lifetime parameter, e.g.,
struct Iter<'a, T> { ptr: *const T, _marker: PhantomData<&'a T> }. The lifetime ensures the iterator does not outlive the collection.
You will write annotations most often in library code—functions that accept borrowed data and return a reference derived from it. Application code tends to rely on owned types (like String) and can often avoid explicit lifetimes altogether.
Summary
Lifetime annotations capture the relationship between the lifetimes of references that flow through a function, struct, or method. They do not create or extend lifetimes; they encode the minimal guarantee the borrow checker must enforce.
The key insight: when a function returns a reference, Rust must know from which input that reference originates, so it can verify that the source data lives long enough. Annotations provide that link.
The syntax—'a after an &, declared in angle brackets—is small, but the thinking behind it is fundamental to Rust’s memory safety model. Once you internalize the idea that 'a names the overlapping scope of several references, you can apply annotations mechanically to satisfy the compiler, and then gradually design APIs that naturally satisfy lifetime constraints.