Lifetimes

Course 2 · Ch 1
Lifetimes
Giving the borrow checker a hint when a reference's relationship to its data isn't obvious from the code alone

Chapter 4 of Course 1 established that the borrow checker verifies references never outlive their data. Most of the time it does this invisibly. Occasionally — when a function's signature involves multiple references whose relationship to each other isn't clear from the code alone — the compiler needs an explicit hint. That hint is a lifetime annotation.

Why the Borrow Checker Sometimes Needs Help

fn longest(x: &str, y: &str) -> &str { if x.len() > y.len() { x } else { y } } // compile error: missing lifetime specifier

This won't compile as written. The compiler can't determine which input's lifetime the returned reference should be tied to — it depends on which branch actually runs, and that's a runtime decision the compiler can't resolve at compile time without more information.

Lifetime Annotation Syntax

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } } let s1 = String::from("long string"); let s2 = String::from("short"); let result = longest(s1.as_str(), s2.as_str());

'a — an apostrophe plus a name, conventionally a single lowercase letter — is a lifetime parameter. This is a common early misconception worth stating plainly: lifetime annotations don't change how long anything lives. They describe a relationship that already exists in the code — here, that the returned reference is valid for at most as long as the shorter of x and y's actual lifetimes.

Lifetime Elision Rules

Most functions with references never need explicit lifetime annotations at all — the compiler applies three elision rules automatically:

  1. Each reference parameter gets its own lifetime.
  2. If there's exactly one input lifetime, it's assigned to every output lifetime.
  3. If one parameter is &self or &mut self (a method), self's lifetime is assigned to every output.
impl Sentence { fn first_word(&self) -> &str { // no explicit lifetime needed — rule 3 applies self.text.split_whitespace().next().unwrap_or("") } }

This is exactly why every &self method returning a reference back in Course 1 (Chapter 5) never needed an explicit lifetime — elision was silently handling it via rule 3 the entire time.

Struct Lifetimes

A struct that holds a reference, rather than an owned value, needs its own lifetime parameter:

struct Excerpt<'a> { part: &'a str, }

This ties the struct instance's own validity to the reference it holds — if the referenced data were dropped before the struct itself, that's a compile error, exactly Course 1 Chapter 4's dangling-reference protection, now extended to cover structs.

The 'static Lifetime

'static is a special, reserved lifetime meaning "valid for the entire duration of the program." String literals are &'static str, since they're baked directly into the compiled binary and never get dropped.

SituationAnnotation Needed?
One input reference, one output referenceNo — elision rule 2 handles it
&self method returning a referenceNo — elision rule 3 handles it
Multiple input references feeding one outputYes — the compiler can't infer the relationship
A struct holding a referenceYes — the struct needs its own lifetime parameter

Lifetime Annotation ('a)

Describes an existing relationship between references' validity — never changes how long anything actually lives.

Elision Rules

Three automatic rules that cover the vast majority of real functions with no annotation required at all.

Struct Lifetimes

A struct storing a reference needs a lifetime parameter, tying the struct's validity to the data it borrows.

'static

Valid for the whole program's duration — string literals are the most common example.

Zero Runtime Cost — Again
Lifetimes are erased entirely at compile time, exactly like the generic types Course 3 will cover — they exist purely for the compiler's own analysis and leave nothing behind in the compiled binary. This is another concrete piece of the same story Course 1 Chapter 4 told about the borrow checker itself: real safety analysis, zero runtime cost.
'static Is Almost Never the Right Quick Fix
It's tempting, when a lifetime error appears, to slap 'static on everything until the compiler stops complaining. This almost never actually solves the underlying ownership problem — it just forces data to live forever, which is often not what's actually wanted, and sometimes isn't even possible for the data in question. A 'static requirement appearing where it doesn't obviously belong is usually a sign the real fix lies elsewhere — often in how ownership is structured, not in the lifetime annotation itself.

Coding Challenges

Challenge 1

Write a function shortest<'a>(x: &'a str, y: &'a str) -> &'a str returning whichever string slice is shorter, following this chapter's longest example as a template.

📄 View solution
Challenge 2

Define a struct FirstLine<'a> holding a single field line: &'a str, and a method fn announce(&self) -> &str that returns self.line. Explain, citing the specific elision rule, why announce needs no explicit lifetime annotation even though the struct itself does.

📄 View solution
Challenge 3

Explain why changing a function's return type from &'a str to &'static str to silence a lifetime error is usually the wrong fix — describe what actually goes wrong for the caller if the underlying data genuinely isn't valid for the whole program's lifetime.

📄 View solution

Chapter 1 Quick Reference

  • 'a — a lifetime parameter; describes an existing relationship between references, never changes actual lifetimes
  • Elision rule 1: each reference parameter gets its own lifetime
  • Elision rule 2: exactly one input lifetime → assigned to all outputs
  • Elision rule 3: a &self/&mut self parameter's lifetime → assigned to all outputs
  • struct Name<'a> { field: &'a T } — a struct holding a reference needs its own lifetime parameter
  • 'static — valid for the whole program's duration; string literals are &'static str
  • Reaching for 'static to silence an error usually hides a real ownership-structure problem instead of fixing it
  • Lifetimes are erased at compile time — zero runtime cost, purely a compiler-analysis tool
  • Next chapter: Traits — trait definitions, default implementations, trait bounds, and a contrast with Go's interfaces