Rust Lifetimes Cheat Sheet
Quick-reference for lifetime annotations, elision rules, and common patterns. Each section includes copy-ready snippets with inline output comments.
Lifetime Annotation Syntax
Lifetime annotations describe how long references are valid relative to each other. They do not change how long data lives.
&i32 // a reference (lifetime inferred)
&'a i32 // a reference with explicit lifetime 'a
&'a mut i32 // a mutable reference with explicit lifetime 'afn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}The annotation says: the return value lives at least as long as the shorter of x and y.
fn first_of<'a, 'b>(x: &'a str, _y: &'b str) -> &'a str {
x // return borrows from x only
}Lifetime Elision Rules
Three rules let the compiler infer lifetimes automatically. You only annotate when these rules are not enough.
// The compiler sees:
fn foo(x: &str, y: &str)
// as:
fn foo<'a, 'b>(x: &'a str, y: &'b str)// No annotation needed:
fn first_word(s: &str) -> &str {
&s[..s.find(' ').unwrap_or(s.len())]
}
// Compiler infers: fn first_word<'a>(s: &'a str) -> &'a strstruct Parser { input: String }
impl Parser {
// No annotation needed: &self's lifetime applies to return
fn first_token(&self) -> &str {
&self.input[..3]
}
}// ERROR (E0106): compiler cannot infer which input the return borrows from
// fn longest(x: &str, y: &str) -> &str { ... }
// FIX: annotate explicitly
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}Lifetimes in Structs
A struct holding a reference needs a lifetime parameter. The struct cannot outlive its borrowed data.
struct Excerpt<'a> {
part: &'a str,
}
let novel = String::from("Call me Ishmael. Some years ago...");
let excerpt = Excerpt { part: &novel[..16] };
println!("{}", excerpt.part); // => "Call me Ishmael."
// excerpt cannot outlive novelstruct Pair<'a, 'b> {
first: &'a str,
second: &'b str,
}Use separate lifetimes when references come from independent sources.
impl<'a> Excerpt<'a> {
fn part(&self) -> &str {
self.part // elision rule 3: returns &self's lifetime
}
fn announce(&self, announcement: &str) -> &str {
println!("Attention: {announcement}");
self.part // elision rule 3 applies
}
}'static Lifetime
Two meanings: &'static T is a reference valid for the entire program. T: 'static means T contains no short-lived borrows.
let s: &'static str = "hello world";
// The string data is embedded in the binary -- lives forever// String satisfies 'static because it owns its data
fn send_to_thread<T: Send + 'static>(val: T) {
std::thread::spawn(move || {
println!("{val:?}");
});
}
send_to_thread(String::from("hello")); // OK: String is 'static
// send_to_thread(&local_string); // ERROR: &str is not 'staticT: 'static does not mean "lives forever". It means the type does not borrow anything short-lived.
// const strings are &'static str
const APP_NAME: &str = "myapp";
// Static mutable data requires unsafe or sync primitives
use std::sync::LazyLock;
static CONFIG: LazyLock<String> = LazyLock::new(|| {
std::fs::read_to_string("config.toml").unwrap_or_default()
});Non-Lexical Lifetimes (NLL)
Borrows end at their last use, not at the closing }. This allows more code to compile since Rust 2018.
let mut v = vec![1, 2, 3];
let first = &v[0];
println!("{first}"); // last use of first -- borrow ends here
v.push(4); // OK: no active borrowlet mut data = String::from("hello");
let r = &data;
println!("{r}"); // NLL: borrow of data ends here
data.push_str(" world"); // OK in Rust 2018+
println!("{data}");let mut map = std::collections::HashMap::new();
map.insert("key", vec![1, 2, 3]);
// If you get E0502, move the immutable use before the mutation:
let len = map.get("key").map_or(0, |v| v.len()); // borrow ends
map.entry("key").or_default().push(4); // mutation OKLifetime Bounds
Express relationships between lifetimes. 'a: 'b means 'a outlives 'b. T: 'a means T contains no references shorter than 'a.
fn first<'a, 'b>(x: &'a str, _y: &'b str) -> &'a str
where
'a: 'b, // 'a lives at least as long as 'b
{
x
}struct Ref<'a, T: 'a> {
data: &'a T,
}
// T must not contain references shorter than 'a// thread::spawn requires 'static because the thread may outlive the caller
std::thread::spawn(move || {
// closure must own all its data -- no borrowed references
});Common Lifetime Patterns
Frequently occurring patterns for working with lifetimes in real code.
fn longer<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() { a } else { b }
}fn first<'a>(a: &'a str, _b: &str) -> &'a str {
a // output lifetime tied to first parameter only
}struct Config {
name: String,
}
impl Config {
fn name(&self) -> &str {
&self.name // elision: lifetime tied to &self
}
}fn apply<'a>(s: &'a str, f: impl Fn(&'a str) -> &'a str) -> &'a str {
f(s)
}Common Lifetime Errors
Quick fixes for the most frequent lifetime-related compiler errors.
// ERROR: two inputs, one output -- ambiguous
// fn pick(a: &str, b: &str) -> &str { a }
// FIX: annotate which input the output borrows from
fn pick<'a>(a: &'a str, _b: &str) -> &'a str { a }// ERROR: local String is dropped at function end
// fn make() -> &str {
// let s = String::from("hi");
// &s
// }
// FIX: return owned value
fn make() -> String {
String::from("hi")
}// ERROR: x is dropped before r is used
// let r;
// { let x = 5; r = &x; } // x dropped here
// println!("{r}");
// FIX: move x to the same or wider scope
let x = 5;
let r = &x;
println!("{r}");// ERROR: String temporary dropped at semicolon
// let s: &str = &String::from("hello");
// FIX: bind the temporary first
let owned = String::from("hello");
let s: &str = &owned;Lifetimes with Traits
Trait objects have implicit 'static lifetime. Override with explicit lifetime bounds.
// These are equivalent:
fn takes_box(b: Box<dyn std::fmt::Display>)
fn takes_box(b: Box<dyn std::fmt::Display + 'static>)fn print_it<'a>(item: &'a dyn std::fmt::Display) {
println!("{item}");
}
// Or with Box:
fn store_it<'a>(item: Box<dyn std::fmt::Display + 'a>) {
println!("{item}");
}// for<'a> means: "for any lifetime 'a"
fn apply(f: impl for<'a> Fn(&'a str) -> &'a str, s: &str) -> String {
f(s).to_string()
}HRTB is advanced. You'll see it in closure bounds and framework APIs.
Can you write this from memory?
Given `let s = String::from("hello");`, create an immutable reference `r` to `s`.