cohort_07 · enrolling now · 23 of 32 seats remaining starts jan_13_2026 this lesson is free · no email required 4 mentor seats · 19 single seats cohort_07 · enrolling now
free · no email required·lesson_01·week_01

Lifetimes,
from first
principles. // l_01

A taste of the cohort. We are going to deconstruct one lifetime story most Rust developers have hit and waved away — and use it as a starting point for the variance, sub-typing, and HRTB rules we cover in week 01.

Below is the actual session-1 lesson. You can edit and "run" the example yourself. No email, no signup, no nag wall. If you want the recording, the office hours, and lesson_02 onwards, see the cohort_07 enrollment page.

// duration ~28 min reading + lab // prereq 6+ months production rust // instructor vela chen // next lesson_02 (paid)

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.

example_01.rs// 14 lines · won't compile
1
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:

example_02.rs · the "bad" fix+ compiles, − wrong
1
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.

example_03.rs · the "good" fix+ compiles, + correct
1
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.

playground · example_01.rs
cargo build · output
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 T in '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 PhantomData permutations.

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:

what comes next

Want lessons
02 → 48?

This was lesson_01 of 48. The full cohort runs twelve weeks, costs $1,200 (or $1,800 with weekly 1:1 mentorship from Vela), and is refundable through end of week 02. Cohort_07 starts january 13, 2026 with 23 of 32 seats remaining.

// l_02 · week 01

Variance & sub-typing

The four flavors, the soundness arguments, the exercises. Reading: aaron turon's 2014 post on invisible variance.

enrolled cohort only
// l_03 · week 01

Higher-ranked trait bounds

Reading and writing for<'a> Fn(&'a T), when you need it, when you don't. The closure-trait HRTB story.

enrolled cohort only
// l_04 · week 01

'static, reconsidered

What 'static actually is, when it leaks, and three patterns that avoid it entirely. The exercise refactors a real production crate.

enrolled cohort only