step 01 · 4 minThe thing you've already seen.
Below is a function that won't compile. Most Rust developers have written something shaped like this in their first six months. The compiler error says E0597 — "borrowed value does not live long enough" — and most of us, the first time, googled the error, copied a fix, and moved on without thinking too hard about why.
Today we are going to think hard about why. The answer involves four ideas: variance, sub-typing, the elision rules, and the question of whether the function signature even makes sense. Most tutorials get to the elision rules and stop. We're going to keep going.
2
3
4
5
6
7
8
9
10
11
12
13
14
fn longest<'a>( x: &'a str, y: &'a str, ) -> &'a str { if x.len() > y.len() { x } else { y } } fn main() { let mut r = ""; { let tmp = String::from("hi"); r = longest("hello", &tmp); } // tmp dropped here println!("{}", r); // E0597 }
step 02 · 6 minThe bad fix, the okay fix, and the good fix.
The bad fix — and the one most tutorials show — is to relax the lifetime bound:
2
3
4
5
6
7
// works because we're calling with an &'static and a &'short // the trick: 'b: 'a means 'b outlives 'a, so we can // downcast 'b to 'a inside the function. covariance. fn longest<'a, 'b: 'a>( x: &'a str, y: &'b str, ) -> &'a str { if x.len() > y.len() { x } else { y } }
This compiles. It even runs. But it papers over a deeper question: does this function signature even make sense? Why does the caller want a borrowed string back instead of an owned String or Cow<str>? In most real codebases, the answer is "it doesn't" and the right fix is to change the call site.
2
3
4
5
6
7
8
fn main() { // hoist the temporary to the longer scope let tmp = String::from("hi"); let r = longest("hello", &tmp); println!("{}", r); // ✓ } // or — return Cow<str> — see step 04 below
"The compiler isn't asking you to add lifetimes. It's asking you why your function takes those references in the first place."
step 03 · 8 minNow edit it yourself.
The playground below is a stylized in-browser editor — try changing the lifetime annotations, change the call site, change the return type to String and see what the compiler says. The "run" button simulates what cargo run would print.
error[E0597]: `tmp` does not live long enough --> src/main.rs:11:24 | 11 | r = longest("hello", &tmp); | ^^^^ borrowed value does not live long enough 12 | } // tmp dropped here | - `tmp` dropped here while still borrowed 13 | println!("{}", r); | - borrow later used here // help: consider hoisting `tmp` outside the inner scope, or // returning `Cow<str>` so the caller doesn't need to keep `tmp` alive.
step 04 · 6 minThe variance rules behind the curtain.
Why did 'b: 'a work? Because &'b T is covariant in 'b — meaning a longer-lived reference can be safely passed where a shorter-lived one is expected. The compiler shrinks 'b down to 'a at the call site. This is a sub-typing relationship — and Rust has exactly four variance flavors:
- covariant —
&'a Tin'a. Longer lifetimes safely flow into shorter ones. - contravariant — function-argument positions; rare in practice but real.
- invariant —
&'a mut T. Cannot widen or narrow. The cost of mutable aliasing rules. - bivariant — phantom-only edge case for
PhantomDatapermutations.
The reason &'a mut T is invariant is the same reason multi-threaded mutability without locks is unsound: if mutable references could be widened, you could cast a &'short mut T into a &'long mut T and use it after the original borrow expired. Variance is not a syntactic curiosity — it's a soundness contract.
step 05 · 4 minWhat this opens up.
If this 28 minutes felt like the level of detail you wish more Rust resources operated at, that is what the rest of the cohort is. We do this — exactly this depth, with code on the table — for forty-eight more lessons across twelve weeks. By week 03 we have built our own Future type. By week 04 we have a working async runtime. By week 12 we have all shipped a no_std embedded HAL.
If this was already in your wheelhouse — great, you'll get more out of weeks 04 onwards (Pin, atomics, lock-free, no_std). If it stretched you, that's the right place to be. Two ways to keep going: