Rust Lifetimes Cheat Sheet
Explains lifetime annotations, lifetime elision rules, structs holding references, and how the borrow checker uses lifetimes to prevent dangling references.
Basic Lifetime Annotation
Annotating function signatures so the borrow checker can verify references.
// 'a says: the returned reference lives at least as long as both x and yfn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y }}fn main() { let s1 = String::from("long string"); let result; { let s2 = String::from("short"); result = longest(s1.as_str(), s2.as_str()); println!("Longest: {}", result); // must be used before s2 drops }}
Lifetime Elision Rules
When the compiler can infer lifetimes without explicit annotations.
- Rule 1- Each elided input reference gets its own distinct lifetime parameter
- Rule 2- If there's exactly one input lifetime, it's assigned to all elided output lifetimes
- Rule 3- If a parameter is &self or &mut self, its lifetime is assigned to all elided output lifetimes
- fn first_word(s: &str) -> &str- No annotations needed; elision infers the output borrows from s
- When elision fails- The compiler requires explicit 'a annotations when multiple refs could be the source
Structs Holding References
A struct cannot outlive the data it borrows.
// ImportantExcerpt cannot outlive the string slice it referencesstruct ImportantExcerpt<'a> { part: &'a str,}impl<'a> ImportantExcerpt<'a> { fn announce_and_return(&self, announcement: &str) -> &str { println!("Attention: {}", announcement); self.part }}let novel = String::from("Call me Ishmael. Some years ago...");let first_sentence = novel.split('.').next().unwrap();let excerpt = ImportantExcerpt { part: first_sentence };
Lifetimes with Generics
Combining lifetime parameters, generic types, and trait bounds.
use std::fmt::Display;// Combine a lifetime, a generic type, and a trait boundfn longest_with_announcement<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a strwhere T: Display,{ println!("Announcement! {}", ann); if x.len() > y.len() { x } else { y }}// T: 'a means: any references inside T must outlive 'astruct Wrapper<'a, T: 'a> { value: &'a T,}
The 'static Lifetime
The special lifetime meaning "valid for the whole program".
- 'static (reference)- The reference is valid for the entire duration of the program
- String literals- &str literals like "hello" are 'static; stored directly in the binary
- 'static bound on generics- Means T contains no non-'static references (owned data qualifies)
- Common misuse- Don't reach for 'static to "fix" a lifetime error; it often just hides a real ownership issue
Higher-Ranked Trait Bounds (HRTB)
for<'a> lets a bound apply to a closure or trait for every possible lifetime, not just one fixed at the call site.
// F must work for ANY lifetime 'a the caller might supply, not one chosen up frontfn apply_to_str<F>(f: F) -> Stringwhere F: for<'a> Fn(&'a str) -> String,{ let owned = String::from("hello"); f(&owned)}fn main() { let result = apply_to_str(|s: &str| s.to_uppercase()); println!("{result}"); // Fn(&str) -> String is actually sugar for the HRTB form above; // the elided lifetime is implicitly for<'a>}
Lifetime Subtyping & Variance
A longer lifetime is a subtype of a shorter one, so &'long T coerces to &'short T automatically (covariance).
fn shorten<'short, 'long: 'short>(r: &'long str) -> &'short str { r // 'long outlives 'short, so this coercion is always sound}fn main() { let s = String::from("static-ish"); { let short_lived = 5; let r: &i32 = &short_lived; // &'long str is a subtype of &'short str: covariant in its lifetime let _use_r = r; } println!("{s}");}// Cell<&'a T> and fn(&'a T) are NOT covariant in 'a (invariant/contravariant// respectively) — this is why interior mutability + lifetimes gets tricky.
Multiple Distinct Lifetime Parameters
Giving parameters separate lifetimes when the compiler must not assume they share a lifetime.
// x and y can have completely different lifetimes; only the return// borrows from x, so y's lifetime need not constrain the outputfn first_with_note<'a, 'b>(x: &'a str, y: &'b str) -> &'a str { println!("note: {}", y); x}fn main() { let a = String::from("long-lived"); let result; { let b = String::from("short-lived note"); result = first_with_note(a.as_str(), b.as_str()); // b can drop right after this call; result never borrowed from it } println!("{result}");}
Common Lifetime Errors & Fixes
Recurring borrow-checker complaints and the idiomatic way to resolve each.
- "cannot return reference to local variable"- The function is trying to return a borrow of stack data it owns; return an owned value (String, Vec<T>) instead
- "borrowed value does not live long enough"- The referent is dropped before the reference is used; extend the referent's scope or clone the data
- "lifetime may not live long enough" (generic fn)- Add an explicit where 'a: 'b bound to state that one lifetime must outlive another
- Self-referential structs- A struct cannot hold a reference into its own fields; use indices/handles, Rc<RefCell<T>>, or the ouroboros/self_cell crates instead
- "missing lifetime specifier" on structs- Any struct field that is a reference must be parameterized: struct Foo<'a> { data: &'a str }
- Iterator adaptor lifetime leaks- A closure capturing a short-lived reference and returned via impl Iterator ties the iterator's lifetime to that capture — collect() into an owned type to escape it
Lifetime Bounds on Trait Objects
dyn Trait defaults to 'static; an explicit bound is required when the object borrows data.
trait Greeter { fn greet(&self) -> String;}struct Named<'a> { name: &'a str,}impl<'a> Greeter for Named<'a> { fn greet(&self) -> String { format!("Hello, {}!", self.name) }}// Without `+ 'a`, this would default to `dyn Greeter + 'static`,// which Named<'a> cannot satisfy unless 'a: 'staticfn make_greeter<'a>(name: &'a str) -> Box<dyn Greeter + 'a> { Box::new(Named { name })}fn main() { let s = String::from("Rustacean"); let g = make_greeter(&s); println!("{}", g.greet());}
Lifetime parameters don't change how long a value lives — they only describe constraints to the borrow checker so it can verify references don't outlive their data. If you're fighting lifetimes, consider whether owning the data (cloning, or using Rc) is simpler than threading references through.