Rust · lesson 10 of 30
Beginner
15- 1 Introduction to Rust
- 2 Setting Up Rust
- 3 Variables in Rust
- 4 Data Types in Rust
- 5 Operators in Rust
- 6 Control Flow in Rust
- 7 Functions in Rust
- 8 Ownership in Rust
- 9 Borrowing and References
- 11 Structs in Rust
- 12 Enums in Rust
- 13 Pattern Matching in Rust
- 17 Collections in Rust
- 25 Testing in Rust
- 26 File I/O in Rust
Intermediate
9Advanced
6Lifetimes in Rust
Understand Rust lifetimes: annotation syntax, elision rules, lifetimes in structs, 'static, and the real compiler errors lifetimes prevent.
By SkillByExample · Updated
What Are Lifetimes?
Every reference in Rust has a lifetime — the stretch of code for which the reference is valid. Most of the time the compiler infers lifetimes automatically. You only need to write explicit lifetime annotations when the compiler cannot figure out the relationship between the lifetimes of multiple references on its own.
Lifetimes exist to prevent dangling references. Here r outlives the value it points to:
fn main() {
let r;
{
let x = 5;
r = &x;
} // x is dropped here
println!("{}", r); // r would point to freed memory
}
$ rustc lifetimes_what_are_lifetimes.rs
error[E0597]: `x` does not live long enough
--> lifetimes_what_are_lifetimes.rs:5:13
|
4 | let x = 5;
| - binding `x` declared here
5 | r = &x;
| ^^ borrowed value does not live long enough
6 | } // x is dropped here
| - `x` dropped here while still borrowed
7 | println!("{}", r); // r would point to freed memory
| - borrow later used here
The compiler rejects the program: x is dropped at the inner }, but r — which borrows it — is still used afterwards. Delete the println! and the program compiles, because a borrow only has to be valid for as long as it is actually used, not for the whole scope of the variable holding it.
Lifetime Annotation Syntax
Lifetime parameters are written with a leading apostrophe: 'a, 'b, 'input, etc. They appear after & in reference types and in angle brackets on function/struct signatures. A function that returns one of two borrowed inputs needs one:
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() { x } else { y }
}
fn main() {
println!("{}", longest("long string", "xyz"));
}
$ rustc lifetimes_lifetime_annotation_syntax.rs
error[E0106]: missing lifetime specifier
--> lifetimes_lifetime_annotation_syntax.rs:1:33
|
1 | fn longest(x: &str, y: &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 `x` or `y`
help: consider introducing a named lifetime parameter
|
1 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
| ++++ ++ ++ ++
The compiler cannot tell whether the returned reference borrows from x or from y, so it cannot check how long the result may be used. Annotating both inputs and the output with the same 'a answers that question:
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");
let result;
{
let s2 = String::from("xyz");
result = longest(s1.as_str(), s2.as_str());
println!("longest: {}", result); // OK — result used inside s2's scope
}
}
$ rustc lifetimes_lifetime_annotation_syntax_2.rs && ./lifetimes_lifetime_annotation_syntax_2
longest: long string
The annotation 'a says: “the returned reference is valid only as long as both x and y are” — in practice, the shorter of the two. The annotation doesn’t change how long anything lives; it lets the compiler reject a call site where the result would be used after either input is gone.
Lifetime Elision
Rust has three elision rules that let the compiler infer lifetimes in common cases:
- Each reference parameter gets its own lifetime.
- If there is exactly one input lifetime, it is assigned to all output lifetimes.
- If one of the inputs is
&selfor&mut self, its lifetime is assigned to all output lifetimes.
Both functions below compile with no annotations at all — rule 2 covers first_word, rule 3 covers get_name:
struct User {
name: String,
}
impl User {
// rule 3: inferred as fn get_name<'s>(&'s self) -> &'s str
fn get_name(&self) -> &str {
&self.name
}
}
// rule 2: inferred as fn first_word<'a>(s: &'a str) -> &'a str
fn first_word(s: &str) -> &str {
let i = s.find(' ').unwrap_or(s.len());
&s[..i]
}
fn main() {
let user = User { name: String::from("Ada Lovelace") };
println!("{}", user.get_name());
println!("{}", first_word(user.get_name()));
}
$ rustc lifetimes_lifetime_elision.rs && ./lifetimes_lifetime_elision
Ada Lovelace
Ada
longest in the previous section fails precisely because none of these rules apply: it has two reference inputs and no self, so rule 2 and rule 3 both stay silent and the output lifetime is left undetermined.
Lifetimes in Structs
If a struct holds a reference, it must have a lifetime annotation to guarantee the struct doesn’t outlive the data it references:
struct Important<'a> {
text: &'a str,
}
impl<'a> Important<'a> {
fn announce(&self, topic: &str) -> &str {
println!("Attention regarding {}: {}", topic, self.text);
self.text
}
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let i = novel.find('.').unwrap_or(novel.len());
let important = Important { text: &novel[..i] };
let returned = important.announce("openings");
println!("returned: {}", returned);
}
$ rustc lifetimes_lifetimes_in_structs.rs && ./lifetimes_lifetimes_in_structs
Attention regarding openings: Call me Ishmael
returned: Call me Ishmael
The lifetime 'a ties Important to novel: the struct cannot be used after novel is dropped. announce returns &str with no annotation — elision rule 3 gives it the lifetime of &self.
Multiple Lifetime Parameters
When references have genuinely independent lifetimes, give them separate parameters. Here the result only ever comes from the strings, never from the slice that holds them:
fn first_or_default<'a, 'b>(slice: &'a [&'b str], default: &'b str) -> &'b str {
slice.first().copied().unwrap_or(default)
}
fn main() {
let picked;
{
let names = vec!["alice", "bob"]; // the Vec is dropped at the end of this block
picked = first_or_default(&names, "nobody");
}
println!("{}", picked); // still fine: picked borrows a 'static literal, not the Vec
}
$ rustc lifetimes_multiple_lifetime_parameters.rs && ./lifetimes_multiple_lifetime_parameters
alice
Because the return type uses 'b, not 'a, the result is allowed to outlive the Vec it was read from. Collapse both parameters into one 'a and this program no longer compiles — the result would be tied to names, which is gone by the time println! runs. Keep annotations minimal, but introduce a new parameter when the relationships genuinely differ.
The 'static Lifetime
'static means the reference can be valid for the entire program. String literals are baked into the binary, so they are always 'static:
fn needs_static(s: &'static str) {
println!("{}", s);
}
fn main() {
let literal: &'static str = "I have a static lifetime";
needs_static(literal);
let owned = String::from("I live on the heap");
let leaked: &'static str = Box::leak(owned.into_boxed_str());
needs_static(leaked);
}
$ rustc lifetimes_the_static_lifetime.rs && ./lifetimes_the_static_lifetime
I have a static lifetime
I live on the heap
Box::leak turns an owned heap value into a 'static reference by promising never to free it — the memory is reclaimed only when the process exits, so use it sparingly. Passing &owned directly to needs_static would not compile: a borrow of a local String cannot be 'static.
Common sources of 'static data:
- String literals
Box::leak— leaks heap memory to produce a'staticreferencestaticitems and lazily-initialized statics (std::sync::OnceLock,std::sync::LazyLock)
Do not use 'static bounds just to avoid thinking about lifetimes — it is often too restrictive.
Lifetime Bounds on Generics
Lifetime parameters and generic type parameters can appear in the same signature:
use std::fmt::Display;
fn longest_with_announcement<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a str
where
T: Display,
{
println!("Announcement: {}", ann);
if x.len() > y.len() { x } else { y }
}
fn main() {
let winner = longest_with_announcement("hello", "hi", 42);
println!("winner: {}", winner);
}
$ rustc lifetimes_lifetime_bounds_on_generics.rs && ./lifetimes_lifetime_bounds_on_generics
Announcement: 42
winner: hello
T can be any Display type — here an integer — while 'a still governs only the two string references.
Common Lifetime Errors and Fixes
Returning a reference to local data. No lifetime annotation can fix this — the data is gone when the function returns:
fn bad() -> &str {
let s = String::from("hello");
&s
}
fn main() {
println!("{}", bad());
}
$ rustc lifetimes_common_lifetime_errors_and_fixes.rs
error[E0106]: missing lifetime specifier
--> lifetimes_common_lifetime_errors_and_fixes.rs:1:13
|
1 | fn bad() -> &str {
| ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`
|
1 | fn bad() -> &'static str {
| +++++++
help: instead, you are more likely to want to return an owned value
|
1 - fn bad() -> &str {
1 + fn bad() -> String {
|
The compiler reports the missing lifetime, but its help text points at the real fix: return an owned String instead of a reference.
fn good() -> String {
String::from("hello")
}
fn main() {
println!("{}", good());
}
$ rustc lifetimes_common_lifetime_errors_and_fixes_2.rs && ./lifetimes_common_lifetime_errors_and_fixes_2
hello
A struct holding a reference without a lifetime parameter:
struct Excerpt {
text: &str,
}
fn main() {
let e = Excerpt { text: "hi" };
println!("{}", e.text);
}
$ rustc lifetimes_common_lifetime_errors_and_fixes_3.rs
error[E0106]: missing lifetime specifier
--> lifetimes_common_lifetime_errors_and_fixes_3.rs:2:11
|
2 | text: &str,
| ^ expected named lifetime parameter
|
help: consider introducing a named lifetime parameter
|
1 ~ struct Excerpt<'a> {
2 ~ text: &'a str,
|
The fix is exactly what the compiler suggests: struct Excerpt<'a> { text: &'a str }, as in the Important example above.
Lifetimes are Not Runtime Costs
Lifetime annotations are purely compile-time constructs. They generate zero machine code. They are the compiler’s way of tracking how long references are valid, not runtime metadata.
Summary
| Concept | Meaning |
|---|---|
'a | A named lifetime parameter |
fn f<'a>(x: &'a T) -> &'a T | Output reference lives as long as input |
struct S<'a> { r: &'a T } | Struct cannot outlive the reference it holds |
'static | Reference valid for the entire program |
| Elision | Compiler infers lifetimes for common patterns |
| No annotation needed | When elision rules fully determine all lifetimes |
Practice
-
In the working
longestexample, moveprintln!("longest: {}", result);out of the inner block so it runs afters2is dropped. What does the compiler say?Solution
error[E0597]: `s2` does not live long enough ... 11 | } | - `s2` dropped here while still borrowed 12 | println!("longest: {}", result); | ------ borrow later used here'atiesresultto the shorter-lived input. It doesn’t matter thats1happens to be the longer string at runtime — the compiler checks lifetimes from the signature, not from which branch theiftakes. -
Keep the
println!outside the block, but changes2to a string literal:let s2 = "xyz";and passs2directly. Does it compile now?Solution
Yes — it prints
longest: long string. A literal is&'static str, so the shorter of the two input lifetimes is nows1’s, which lasts until the end ofmain. -
Collapse
first_or_default’s two lifetime parameters into one:fn first_or_default<'a>(slice: &'a [&'a str], default: &'a str) -> &'a str. Keepmainunchanged. What happens?Solution
error[E0597]: `names` does not live long enoughWith a single
'a, the result is tied to the borrow ofnamesitself, which ends when the inner block closes. Two parameters let the signature say the truth: the result comes from the strings ('b), not from theVecholding them ('a). -
Call
needs_static(&owned)withlet owned = String::from("I live on the heap");instead of leaking it. Read the error and explain which phrase names the problem.Solution
error[E0597]: \owned` does not live long enough, with the key labelargument requires that `owned` is borrowed for `‘static`. A localStringis dropped at the end ofmain, so a borrow of it can never satisfy’static`. -
Write
fn last_word(s: &str) -> &strwithout any lifetime annotations, returning everything after the last space. Which elision rule makes it compile?Solution
fn last_word(s: &str) -> &str { let i = s.rfind(' ').map(|i| i + 1).unwrap_or(0); &s[i..] }last_word("Ada Lovelace")returnsLovelaceandlast_word("Ada")returnsAda. Rule 2 applies: there is exactly one input lifetime, so the output gets it.