Step 09 of 17
Lifetimes
Read and write lifetime annotations, apply the elision rules, build structs that borrow, and know when 'static is right.
Every reference in Rust has a lifetime: the stretch of code during which it's valid. Most of the time the compiler works lifetimes out by itself, and you never write them. Lifetime annotations appear when it can't, usually when a function returns a reference and the compiler needs to know which input it borrows from. This lesson explains what annotations mean (and what they don't), the elision rules that let you skip them, structs that hold references, 'static, and how to decide whether to borrow at all.
The problem lifetimes solve
The borrow checker's job is to guarantee no reference outlives the data it points to. Inside one function it can see everything:
fn main() {
let r;
{
let x = 5;
r = &x;
} // x is dropped here
println!("{r}"); // error[E0597]: `x` does not live long enough
}Across function boundaries, it only looks at signatures, never at other functions' bodies. That keeps compilation modular and errors local. But consider:
fn longest(a: &str, b: &str) -> &str {
if a.len() >= b.len() { a } else { b }
}error[E0106]: missing lifetime specifier
--> src/main.rs:1:33
|
1 | fn longest(a: &str, b: &str) -> &str {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `a` or `b`A caller can't know how long the returned reference stays valid without knowing which argument it came from. The signature has to say.
Lifetime annotations
A lifetime parameter is written 'a (any lowercase name after an apostrophe) and declared in angle brackets like a generic type:
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() { a } else { b }
}Read it as: "for some lifetime 'a, both inputs live at least as long as 'a, and the result is valid for 'a." In practice the compiler picks 'a as the overlap of the two inputs' lifetimes, so the result is valid only while both are.
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() { a } else { b }
}
fn main() {
let outer = String::from("a long string");
let result;
{
let inner = String::from("short");
result = longest(&outer, &inner);
println!("{result}"); // fine: both still alive
}
// println!("{result}"); // error: `inner` does not live long enough
}Even though outer is the longer string here, the compiler goes by the signature, which says the result may borrow from either. So result can't be used after inner dies.
If the result only ever comes from one argument, say so, and the other argument is unconstrained:
fn first_word<'a>(text: &'a str, _sep: &str) -> &'a str {
text.split_whitespace().next().unwrap_or("")
}Getting the signature right gives callers more freedom: here the separator can be a temporary.
The elision rules
If you had to annotate every reference, Rust would be miserable. Instead, the compiler applies three elision rules to function signatures:
- Each reference parameter gets its own lifetime:
fn f(a: &str, b: &str)becomesfn f<'a, 'b>(a: &'a str, b: &'b str). - If there's exactly one input lifetime, it's assigned to every output reference:
fn trim(s: &str) -> &strmeans the output borrows froms. - If there's a
&selfor&mut selfparameter, its lifetime is assigned to every output reference: methods returning references are assumed to borrow fromself.
If the rules leave an output lifetime undetermined, you get the "missing lifetime specifier" error and must annotate. That's why longest needs one and first_word(s: &str) -> &str doesn't.
Rule 3 is usually what you want, since a getter like fn name(&self) -> &str returns a piece of the struct. When a method returns a reference into an argument instead, annotate it.
Structs that hold references
A struct that holds a reference needs a lifetime parameter, so the compiler can ensure the struct doesn't outlive what it borrows:
struct Parser<'a> {
input: &'a str,
pos: usize,
}
impl<'a> Parser<'a> {
fn new(input: &'a str) -> Self {
Self { input, pos: 0 }
}
/// Returns the next whitespace-separated token, borrowed from the input.
fn next_token(&mut self) -> Option<&'a str> {
let rest = &self.input[self.pos..];
let start = rest.find(|c: char| !c.is_whitespace())?;
let rest = &rest[start..];
let len = rest.find(char::is_whitespace).unwrap_or(rest.len());
self.pos += start + len;
Some(&rest[..len])
}
}
fn main() {
let source = String::from("let x = 42 ;");
let mut p = Parser::new(&source);
let mut tokens = Vec::new();
while let Some(t) = p.next_token() {
tokens.push(t);
}
drop(p); // the tokens borrow from `source`, not from `p`
println!("{tokens:?}"); // ["let", "x", "=", "42", ";"]
}Notice the return type Option<&'a str>, not Option<&str>. With elision rule 3, &str would borrow from &mut self, and every token would keep the parser mutably borrowed, so the loop couldn't call next_token again while holding earlier tokens. Writing 'a says the tokens borrow from the original input, which outlives the parser. This is the most common real-world reason to write a lifetime by hand.
Borrowing structs like this are great for zero-copy parsers, iterators and views over data. For long-lived application state, owned data is almost always simpler.
'static
'static means "valid for the entire program". Two different things use it:
&'static T, a reference that's valid forever. String literals are&'static strbecause they're baked into the binary. So are references tostaticitems, and memory deliberately leaked withBox::leak.T: 'static, a bound meaning "this type contains no non-static references". An ownedString, aVec<u32>or anArc<Config>all satisfyT: 'static, even though they'll be dropped at some point. It means the value could live forever, because it doesn't borrow anything that might go away.
The second one is what you meet in practice. thread::spawn and tokio::spawn require F: 'static because the new thread or task may outlive the function that created it, so it can't hold references to that function's locals. The fix is almost never to add 'static annotations; it's to move owned data into the closure (move ||), or share it with Arc.
Owned or borrowed?
Lifetimes are a sign you're borrowing. Sometimes the right fix for a lifetime headache is to stop borrowing:
| Situation | Usually |
|---|---|
| Function parameters you only read | Borrow: &str, &[T], &T |
| Return values computed inside the function | Owned: String, Vec<T> |
| Returning a part of an input | Borrow, with a lifetime tying it to that input |
| Struct fields in long-lived state, config, domain models | Owned |
| Short-lived views, parsers, iterators over a buffer | Borrow, with a lifetime parameter on the struct |
| Data shared across threads or tasks | Arc<T> (shared ownership) |
A struct User<'a> { name: &'a str } forces every user of User to carry a lifetime and to keep the source alive. A struct User { name: String } costs an allocation and is far easier to pass around. Start owned; borrow where profiling or API design gives you a reason. Cow<'a, str> ("clone on write") sits in between: it holds either a borrowed or an owned string, useful for functions that usually return their input unchanged but sometimes need to modify it.
Practice
- Write
fn split_key_value<'a>(line: &'a str) -> Option<(&'a str, &'a str)>that splits"key = value"and returns trimmed slices. Then remove the annotations: does it still compile? Which elision rule applies? - Write a struct
Highlights<'a> { text: &'a str }with a methodfn sentences_with(&self, word: &str) -> Vec<&'a str>that returns every sentence (split on'.') containingword. Show that the returned sentences can be used after theHighlightsvalue is dropped, and thatwordcan be a temporary. - Explain why this fails and fix it without
'static:
fn spawn_printer(name: &str) {
std::thread::spawn(|| println!("{name}"));
}Show solution
fn split_key_value(line: &str) -> Option<(&str, &str)> {
let (k, v) = line.split_once('=')?;
Some((k.trim(), v.trim()))
}It compiles without annotations: there's one input lifetime, so rule 2 assigns it to every output reference.
struct Highlights<'a> {
text: &'a str,
}
impl<'a> Highlights<'a> {
fn sentences_with(&self, word: &str) -> Vec<&'a str> {
self.text
.split('.')
.map(str::trim)
.filter(|s| s.contains(word))
.collect()
}
}
fn main() {
let article = String::from("Rust is fast. Rust is safe. Go is simple.");
let found = {
let h = Highlights { text: &article };
h.sentences_with(&String::from("Rust")) // word is a temporary
}; // h is dropped here
println!("{found:?}"); // ["Rust is fast", "Rust is safe"]
}The return type names 'a, the lifetime of text, so the sentences borrow from article, not from h or word. With an elided Vec<&str>, rule 3 would tie the result to &self, and found couldn't outlive h.
- The spawned thread might run after
spawn_printerreturns, whenname(borrowed from the caller) may be gone.thread::spawnrequires a'staticclosure. Move an owned copy in:
fn spawn_printer(name: &str) -> std::thread::JoinHandle<()> {
let name = name.to_string();
std::thread::spawn(move || println!("{name}"))
}Returning the JoinHandle lets the caller wait for it. If the thread only needs to live within the caller's scope, std::thread::scope allows borrowing directly; see the concurrency lesson.
Key takeaways
- A lifetime is the span during which a reference is valid; annotations describe how references relate, they never extend how long anything lives.
- The compiler checks across functions using signatures only, so returned references must say which input they borrow from.
- Three elision rules cover most signatures: one input lifetime, or
&self, flows to the outputs. - Structs holding references need a lifetime parameter; return
&'a Tfrom their methods when results borrow from the source, not fromself. T: 'staticmeans "owns everything it holds". Satisfy it by moving owned data or usingArc, not by annotating references'static.
Check yourself
Quiz
0/4 correct · 4 left
1.Why does fn pick(a: &str, b: &str) -> &str need a lifetime annotation while fn trim(s: &str) -> &str doesn't?
2.What does fn longest<'a>(x: &'a str, y: &'a str) -> &'a str promise?
3.Which of these types satisfies the bound T: 'static?
4.A method fn next_token(&mut self) -> Option<&str> on Parser<'a> keeps the parser mutably borrowed while you hold a token. What's the fix?
Concepts in this lesson
One-page notes on each idea, for review or a quick lookup later.
Finished reading?
Mark it complete to fill in your roadmap.