A lifetime is the scope for which a reference is guaranteed to be
valid. Every reference in Rust has a lifetime — most of the time it's
invisible because the compiler infers it. Lifetime annotations
('a)
don't change how long anything lives — they just describe the
relationships between the lifetimes of multiple references so the
borrow checker can verify none of them outlive the data they point to.
Think of it like this: ownership (Day 01) answers "who owns this data and when does it get dropped?". Lifetimes answer "how long is this reference allowed to be used for, given when the data it points to gets dropped?". Lifetimes are a compile-time-only concept — they add zero runtime cost and disappear completely from the compiled binary.
Most functions don't need explicit 'a annotations
because the compiler applies these 3 elision rules automatically. If after
applying all three the compiler still can't figure out the lifetimes, that's when
you must annotate manually.
Each Reference Param Gets Its Own Lifetime
Every parameter that's a reference gets its own lifetime parameter. fn foo(x: &str, y: &str) becomes fn foo<'a,'b>(x: &'a str, y: &'b str) internally.
One Input → Output Gets That Lifetime
If there's exactly one input lifetime, it's assigned to all output lifetimes. fn foo(x: &str) -> &str becomes fn foo<'a>(x: &'a str) -> &'a str.
&self / &mut self → Output Gets Its Lifetime
If one of the params is &self or &mut self (a method), the lifetime of self is assigned to all output lifetimes — common in builder/getter methods.
fn dangle() -> &String { let s = String::from("hello"); // s created here, owned by dangle() &s // ❌ ERROR: we return a reference to s... } // ...but s is dropped HERE, at the end of the function! // The reference we tried to return would point to freed memory. // error[E0106]: missing lifetime specifier // help: this function's return type contains a borrowed value // with no value for it to be borrowed from fn main() { // let s = dangle(); // would be a dangling reference if this compiled }
sis a localString— its owner is thedanglefunction's stack frame.- When
dangle()returns, its stack frame is popped ands'sDrop::drop()runs — the heap memory holding"hello"is freed. - If Rust allowed
&sto be returned, the caller would hold a pointer to freed memory — a use-after-free bug, the exact thing C/C++ programs suffer from. - The fix: either return the owned
String(move it out, no reference) — or accept a reference as input and return a reference tied to that input's lifetime (next example).
String moves out to the caller — nothing dangles.
fn no_dangle() -> String { // return type is OWNED String, not &String let s = String::from("hello"); s // ✅ ownership MOVES out to the caller — s is not dropped } fn main() { let result = no_dangle(); println!("{}", result); // ✅ "hello" — result owns the data now }
'a exists: longest takes two string slices and returns whichever is longer. The compiler cannot know which one without help — Rule 02 (one input → output) doesn't apply because there are TWO input lifetimes.
// 'a says: the returned reference will live AT MOST as long as // the SHORTER of x's and y's lifetimes. fn 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 is long"); let s2 = String::from("short"); let result = longest(s1.as_str(), s2.as_str()); println!("Longest string is: {}", result); // "long string is long" }
<'a>declares a generic lifetime parameter — just like<T>declares a generic type parameter. It does NOT set a specific duration.x: &'a str, y: &'a strtells the compiler: "treat both inputs as if they live for the same region'a". In practice, Rust picks'a= the smaller (more restrictive) of the two actual lifetimes.-> &'a strmeans: "the returned reference's validity is tied to'a" — so the caller can't use the result longer than the shorter-lived ofs1/s2would allow.- This is purely a compile-time contract. At runtime,
longestjust returns a pointer — no extra checks, no overhead.
longest where one argument's owner is dropped before the result is used. This shows the borrow checker actually enforcing the 'a contract from the previous example.
fn 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 is long"); let result; { let s2 = String::from("short"); // s2 lives only in this inner scope result = longest(s1.as_str(), s2.as_str()); } // s2 dropped here // 'a was forced to shrink to s2's lifetime (the shorter one) // so `result` is only valid INSIDE the block above. println!("Longest string is: {}", result); // ❌ ERROR: `s2` does not live long enough // `result` may still borrow from `s2`, but `s2` is already dropped }
- Because both params share lifetime
'a, the compiler must pick a single'athat's valid for boths1ands2— that's the smaller scope, i.e. the inner block wheres2lives. resulttherefore inherits that shrunk'a— it's only valid inside the inner{ }block, regardless of which branch (xory) actually gets returned at runtime.- The compiler doesn't know at compile time which branch wins (that depends on
.len()at runtime) — so it conservatively assumesresultcould be borrowing froms2, and rejects theprintln!outside the block. - This is the borrow checker trading a little flexibility for a 100% guarantee: no dangling references, ever, in safe Rust.
'a says: "I cannot outlive the data I'm pointing into."
// ExcerptHolder borrows a &str — it does NOT own the string data. // 'a ties the struct's lifetime to the lifetime of that borrowed &str. struct ExcerptHolder<'a> { part: &'a str, } impl<'a> ExcerptHolder<'a> { // Rule 3 (elision): &self's lifetime is assigned to the output fn announce_and_return_part(&self, announcement: &str) -> &str { println!("Attention please: {}", announcement); self.part } } fn main() { let novel = String::from("Call me Ishmael. Some years ago..."); let first_sentence = novel.split('.').next().expect("no '.' found"); let excerpt = ExcerptHolder { part: first_sentence }; // `excerpt` cannot outlive `novel` — enforced at compile time println!("{}", excerpt.part); // "Call me Ishmael" let _ = excerpt.announce_and_return_part("New excerpt!"); }
part: &'a strmeans the struct doesn't own its string data — it's a "view" intonovel's heap allocation, just like the&strfrom Day 01's slices.'aon the struct means: any instance ofExcerptHoldercannot live longer than the datapartpoints to. Ifnovelis dropped whileexcerptstill exists, that's a compile error.impl<'a> ExcerptHolder<'a>— you must declare'aon theimplblock too, matching the struct's definition.- Rule 3 (elision) applies in
announce_and_return_part: because&selfis a parameter, its lifetime is assigned to the output&str— no explicit annotation needed. - This pattern is everywhere in real code: JSON parsers, tokenizers, and config readers that avoid allocating new strings by borrowing slices of the original input.
'static is a special lifetime meaning "this reference can live for the entire duration of the program". String literals are 'static by default because they're baked directly into the compiled binary.
fn main() { // String literals are &'static str — baked into the binary's read-only data segment let s: &'static str = "I live for the whole program"; println!("{}", s); // A function that returns a 'static reference fn get_app_name() -> &'static str { "rust-job-prep" // literal — always valid, no owner to drop } println!("{}", get_app_name()); } // ⚠️ COMMON MISTAKE: 'static does NOT mean "leak memory" or "live forever // no matter what". It means "CAN live that long if needed" — for owned data // like Box, you'd need Box::leak() to actually get a 'static reference, // which intentionally leaks memory. Don't reach for 'static to "fix" a // lifetime error — it usually means restructuring ownership instead.
- String literals (
"...") are embedded in the binary's read-only data section — they exist beforemain()runs and after it ends, so a reference to them is trivially valid for'static. 'staticis the longest possible lifetime — any'acan be a subset of'static, but not the reverse.- Beginners often add
'staticto "fix" a borrow-checker error — this usually just moves the error elsewhere or forces unnecessary heap leaks viaBox::leak/Box::new(...).into()tricks. Treat a'staticrequirement as a signal to re-check ownership design.
'a, 'b) instead of forcing them to be equal — this gives the caller more flexibility.
// 'a and 'b are INDEPENDENT — x and y can have totally different lifetimes. // The output only depends on 'a (x's lifetime), so 'b has no constraint // on the bound 'a: 'b means "'a must outlive 'b" — used when needed. fn first_word_with_logger<'a, 'b>(text: &'a str, logger_tag: &'b str) -> &'a str { println!("[{}] processing text...", logger_tag); // 'b only used here match text.find(' ') { Some(idx) => &text[..idx], None => text, } } fn main() { let sentence = String::from("lifetimes are not that scary"); let word; { let tag = String::from("PARSER"); // short-lived 'b word = first_word_with_logger(&sentence, &tag); } // tag dropped here — but `word` is fine, it only borrows from `sentence` ('a) println!("first word: {}", word); // ✅ "lifetimes" — still valid! }
- Compare this to the earlier
longestexample — there, forcingxandyto share'acaused the output to be artificially restricted by the shorter-lived argument. - Here,
logger_tag: &'b strhas its own independent lifetime. The return type&'a stronly depends on'a(fromtext) — so'bcan end early (whentagis dropped) without affectingword. - This is the practical lesson: only tie lifetimes together if the output genuinely depends on both. Over-constraining with a single
'afor everything is a common beginner mistake that causes unnecessary borrow errors.
Zero-Copy Parsing
Parsers (JSON, HTTP, config files) hold &'a str slices into the original input buffer instead of allocating new Strings for every token — massive performance win on hot paths.
Compile-Time Memory Safety
Lifetimes are how Rust guarantees no dangling pointers without a garbage collector or runtime checks — the safety analysis happens entirely before the binary is built.
Safe Borrowing Across Function Boundaries
Lifetime annotations let APIs express "this returned reference is only valid as long as you keep this input alive" — callers get compile errors instead of production crashes.
Self-Referential Async State Machines
async fn bodies desugar into structs that often borrow their own fields across .await points — the compiler's lifetime + Pin machinery makes this safe.
&'a str fields, avoiding allocation per field&'a [u8] / &'a str input slices threaded through every combinatorRule 2: if there's exactly one input lifetime, it's assigned to all output lifetimes.
Rule 3: if one parameter is
&self or &mut self, its lifetime is assigned to all outputs.If these three rules don't fully determine the lifetimes (e.g. two reference params and no
self, with a reference return type), the compiler requires explicit 'a annotations.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str actually guarantee?
+
x's and y's lifetimes. It does NOT mean x and y must have identical actual lifetimes — the compiler just picks 'a as the intersection (smaller) of the two when checking the call site. This is why the result can become invalid as soon as the shorter-lived argument is dropped, even if the function actually returned the longer-lived one.
'static mean, and is it ever a "fix" for a lifetime error?
+
'static means a reference is valid for the entire remaining duration of the program — the longest possible lifetime. String literals are &'static str because they're embedded in the binary's read-only data.It is rarely the correct "fix" for a lifetime error. Slapping
'static on a type often just moves the problem (now you need a 'static value to satisfy it) or forces a memory leak via Box::leak. The right fix is almost always to restructure ownership — e.g. clone the data, use an owned type, or adjust the struct's lifetime parameters.
struct Excerpt<'a> { part: &'a str }?
+
&'a str) instead of an owned value (String). The lifetime parameter 'a ties the struct's own validity to the validity of the data it borrows — any instance of Excerpt<'a> cannot outlive the string it points into. This is the foundation of zero-copy data structures: parsers, tokenizers, and views into buffers that avoid allocating owned copies.
'a, 'b) instead of one?
+
'a over-constrains the function — the output's lifetime gets unnecessarily tied to whichever input has the shorter lifetime, even if the returned value never actually borrows from it. Splitting into 'a and 'b (and optionally adding a bound like 'a: 'b if one must outlive the other) gives callers more flexibility and avoids spurious borrow-checker errors.
| Pattern | What It Solves | Used In |
|---|---|---|
Higher-Ranked Trait Bounds (HRTB)for<'a> Fn(&'a T) |
Express "this closure must work for any lifetime 'a", not just one fixed lifetime — needed when a trait method's input lifetime varies per call. |
Iterator combinators, callback-based APIs, fn trait bounds |
Lifetime subtyping'a: 'b |
"'a outlives 'b" — used in where clauses when one reference must remain valid for at least as long as another. |
Self-referential builder patterns, generic struct constraints |
Cow<'a, str> |
Combines lifetimes with ownership: borrow (&'a str) when possible, clone to String only when mutation is needed. Avoids unnecessary allocation. |
Config/template processing, string normalization in parsers |
Lifetime in trait objectsBox<dyn Trait + 'a> |
Trait objects normally default to 'static — annotate explicitly when a dyn Trait needs to borrow data with a shorter lifetime. |
Plugin systems, callback registries holding borrowed closures |
Self-referential structs via Pin |
Async fn bodies generate structs whose fields can reference other fields in the same struct — normal lifetimes can't express this; Pin + the generated state machine handle it. |
tokio async tasks, futures combinators |
use std::borrow::Cow; // Returns a borrowed slice if no change is needed, or an owned String // only when mutation is actually required — avoids unnecessary allocation. fn normalize<'a>(input: &'a str) -> Cow<'a, str> { if input.contains(' ') { Cow::Owned(input.replace(' ', "_")) // allocates a new String } else { Cow::Borrowed(input) // zero allocation — just a reference } } fn main() { let a = normalize("already_clean"); // Cow::Borrowed — no allocation let b = normalize("needs spaces"); // Cow::Owned — one allocation println!("{} | {}", a, b); // "already_clean | needs_spaces" // HRTB example: a closure that must accept &T for ANY lifetime 'a // — common in iterator/visitor-style APIs fn apply_to_all(items: &[String], f: F) where F: for<'a> Fn(&'a String) -> usize, { for item in items { println!("len = {}", f(item)); } } apply_to_all(&[String::from("a"), String::from("bb")], |s| s.len()); }