Skip to editor content
learningrust.orglesson 13 of 26

Lifetimes

Lifetimes are Rust's way of ensuring that references are always valid. Every reference in Rust has a lifetime - the scope for which that reference is valid. Most of the time, lifetimes are inferred, but sometimes you need to annotate them explicitly.

What Are Lifetimes?

A lifetime is the scope during which a reference is valid. Consider this:

fn main() {
    let r;                // ---------+-- 'a
                          //          |
    {                     //          |
        let x = 5;        // -+-- 'b  |
        r = &x;           //  |       |
    }                     // -+       |
                          //          |
    // println!("{}", r); // ERROR: x doesn't live long enough
}                         // ---------+

The reference r has lifetime 'a, but it refers to x which only has lifetime 'b. Since 'b is shorter than 'a, using r after the inner block ends won't compile — that is why the println! is commented out. Uncomment it and run the snippet to see the borrow checker reject it with "x does not live long enough"; as written, the program compiles and prints nothing.

Lifetime Annotation Syntax

Lifetime annotations describe relationships between lifetimes:

&i32        // a reference
&'a i32     // a reference with explicit lifetime 'a
&'a mut i32 // a mutable reference with explicit lifetime 'a

When You Need Lifetime Annotations

The compiler needs help when:

  1. A function returns a reference
  2. A struct holds references
  3. Multiple references have ambiguous relationships

Function Signatures

// This won't compile - Rust doesn't know which input's lifetime to use
// fn longest(x: &str, y: &str) -> &str {
//     if x.len() > y.len() { x } else { y }
// }

// Solution: annotate with lifetime 'a
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

fn main() {
    let string1 = String::from("long string");
    let string2 = String::from("short");

    let result = longest(&string1, &string2);
    println!("The longest string is: {}", result);
}

The 'a annotation means: "the returned reference will be valid for the smaller of the two input lifetimes."

Lifetime Elision Rules

Rust has rules for inferring lifetimes so you don't always need annotations:

Rule 1: Each Input Gets Its Own Lifetime

// Written:
fn compare(a: &str, b: &str) { ... }
// Compiler infers a DISTINCT lifetime for each input reference:
fn compare<'a, 'b>(a: &'a str, b: &'b str) { ... }

This rule only ever assigns lifetimes to the inputs; the output, if there is one, is still elided at this point. Rules 2 and 3 are what decide it.

Rule 2: One Input Lifetime = Output Lifetime

// Written:
fn first_word(s: &str) -> &str { ... }
// Compiler infers:
fn first_word<'a>(s: &'a str) -> &'a str { ... }

Rule 3: &self Lifetime = Output Lifetime

impl MyStruct {
    // Written:
    fn get_name(&self) -> &str { ... }
    // Compiler infers:
    fn get_name<'a>(&'a self) -> &'a str { ... }
}

Those three rules are worth turning from syntax into a decision you can actually make. The program below declares three signatures, none of them carrying a single lifetime annotation. One of them — B — is commented out because the compiler refuses it; the other two build fine. Knowing which one fails is the easy half. Before you run it, work out the half that matters: run each signature through the three rules in order and say precisely which rule rescues A, which rescues C, and why every rule runs out on B.

Predict

Three signatures, zero lifetime annotations between them, and B is the one the compiler rejects. Work out the reasoning behind that split: which elision rule lets A compile untouched, which one lets C compile untouched, and which rule B falls short of.

// A: one input reference, one output reference.
fn head(s: &str) -> &str {
  s.split(' ').next().unwrap_or("")
}

// B: two input references, one output reference. Commented out because
// exactly one of these three signatures is rejected, and leaving it in
// would stop the whole file from building.
// fn pick(a: &str, b: &str) -> &str {
//     if a.len() >= b.len() { a } else { b }
// }

// C: &self plus another argument, one output reference.
struct Doc { body: String }
impl Doc {
  fn tail(&self, from: usize) -> &str {
      &self.body[from..]
  }
}

fn main() {
  println!("{}", head("hello world"));
  // println!("{}", pick("aa", "b"));
  println!("{}", Doc { body: String::from("abcdef") }.tail(3));
}

As written the program prints hello and def, and B is the signature that had to be commented out. Uncomment pick and its call and the file stops compiling with error[E0106]: missing lifetime specifier, along with the blunt explanation that the signature "does not say whether it is borrowed from a or b". head is fine because rule 2 applies — one input reference, so the output must borrow from it. tail is fine because rule 3 applies — it is a method, so the output borrows from &self, which is correct since the slice comes out of self.body. Note that from: usize is not a reference and plays no part at all. Annotate pick as fn pick<'a>(a: &'a str, b: &'a str) -> &'a str, uncomment both lines, and the whole file compiles and prints hello, aa, def. The question elision answers is never "how much syntax can I leave out" — it is "can the compiler identify one source for the returned reference". When it can, you write nothing. When it cannot, you have to say.

Structs with References

When a struct holds references, you must annotate lifetimes:

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

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().unwrap();

    let excerpt = ImportantExcerpt {
        part: first_sentence,
    };

    println!("Excerpt: {}", excerpt.part);
}

The annotation 'a means: "an instance of ImportantExcerpt can't outlive the reference it holds."

Methods with Lifetimes

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

impl<'a> ImportantExcerpt<'a> {
    // Lifetime elision: &self lifetime is used for return
    fn level(&self) -> i32 {
        3
    }

    // Return type uses 'a from struct
    fn announce_and_return_part(&self, announcement: &str) -> &'a str {
        println!("Attention please: {}", announcement);
        self.part
    }
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let excerpt = ImportantExcerpt {
        part: novel.split('.').next().unwrap(),
    };

    println!("Level: {}", excerpt.level());
    println!("Part: {}", excerpt.announce_and_return_part("Here it comes!"));
}

Look closely at why announce_and_return_part writes -> &'a str rather than letting elision handle it. Rule 3 would have given the output &self's lifetime, which would tie the returned slice to the struct value — but the data actually lives in the string the struct borrowed, which outlives the struct. Getting that distinction wrong is the single most common real lifetime bug, and it is invisible until a caller tries to keep the slice a moment longer than the struct. The program below has exactly that mistake. It looks completely ordinary; commit a hypothesis about which reference is really being tracked before you change anything:

Debug

This settings reader is supposed to hand out a slice of raw that stays valid after the temporary Settings value is gone. It does not compile: rustc says 'settings does not live long enough'. Nothing in main is wrong. Work out which lifetime the return type is actually promising, then fix the signature.

// A settings reader that hands out slices of the text it was built from.
struct Settings<'a> {
  text: &'a str,
}

impl<'a> Settings<'a> {
  fn new(text: &'a str) -> Settings<'a> {
      Settings { text }
  }

  // Return the value for key, borrowed from the original text.
  fn get(&self, key: &str) -> Option<&str> {
      for line in self.text.lines() {
          let (k, v) = line.split_once('=')?;
          if k.trim() == key {
              return Some(v.trim());
          }
      }
      None
  }
}

fn main() {
  let raw = String::from("host = localhost
port = 8080");

  // The Settings value is temporary scaffolding; the slice it produced is not.
  let port;
  {
      let settings = Settings::new(&raw);
      port = settings.get("port").expect("port should be present");
  }

  assert_eq!(port, "8080", "expected the port slice to outlive the Settings");
  println!("port = {}", port);
}

Expected output: port = 8080

The signature was under-promising. Elision rule 3 tied the returned &str to &self — the borrow of the Settings value — while the slice actually points into self.text, the original string, which outlives that value. So main is right to complain: it wants port after settings is gone, and the signature said that was not allowed. The fix is a single annotation, -> Option<&'a str>, which says the result borrows from the text rather than from the struct. Nothing about the runtime changes; a lifetime annotation only describes a relationship that already holds, it never extends anyone's life. This is also why key stays unannotated: the answer is never borrowed from the string you searched with.

The Static Lifetime

'static means the reference lives for the entire program duration:

fn main() {
    // String literals have 'static lifetime
    let s: &'static str = "I live forever!";

    println!("{}", s);
}

Use 'static sparingly - it's usually a sign you should reconsider your design.

Before the patterns section, pin down what all of this is ultimately in service of. Lifetimes are not a fourth thing on top of ownership and borrowing — they are the machinery that makes one specific earlier rule checkable across a function boundary. Close the page and answer from memory:

Recall

Without scrolling up: Borrowing in Depth gave you a rule about a reference and the value it points to — the one that makes it impossible to hold a reference to something that has been freed. Which statement correctly connects that rule to what lifetime annotations are for?

Lifetimes are the same no-dangling-references rule from Borrowing in Depth, carried across a function boundary. Inside one body the borrow checker can see every scope and compare them itself. A signature, though, is opaque — the caller sees only the signature, never the body — so when a reference comes out, the signature must say which reference going in it is tied to. That is why annotations cluster on exactly two shapes: functions that return references and structs that hold them. And it is why an annotation never changes what the program does at runtime: like a type, it states a relationship the compiler then checks.

Multiple Lifetime Parameters

Sometimes you need multiple lifetime parameters:

fn longest_with_announcement<'a, 'b>(
    x: &'a str,
    y: &'a str,
    ann: &'b str,
) -> &'a str {
    println!("Announcement: {}", ann);
    if x.len() > y.len() { x } else { y }
}

fn main() {
    let s1 = String::from("hello");
    let s2 = String::from("world!");
    let ann = String::from("Comparing strings");

    let result = longest_with_announcement(&s1, &s2, &ann);
    println!("Longest: {}", result);
}

Lifetime Bounds

You can specify that a generic type must live at least as long as a lifetime:

fn print_ref<'a, T>(t: &'a T)
where
    T: std::fmt::Display + 'a,
{
    println!("{}", t);
}

fn main() {
    let x = 5;
    print_ref(&x);
}

Common Lifetime Patterns

Pattern 1: Input/Output Relationship

fn first_word<'a>(s: &'a str) -> &'a str {
    match s.find(' ') {
        Some(pos) => &s[..pos],
        None => s,
    }
}

fn main() {
    let sentence = String::from("hello world");
    let word = first_word(&sentence);
    println!("First word: {}", word);
}

Pattern 2: Struct Holding a Reference

struct Parser<'a> {
    input: &'a str,
    position: usize,
}

impl<'a> Parser<'a> {
    fn new(input: &'a str) -> Parser<'a> {
        Parser { input, position: 0 }
    }

    fn remaining(&self) -> &'a str {
        &self.input[self.position..]
    }
}

fn main() {
    let text = String::from("hello world");
    let parser = Parser::new(&text);
    println!("Remaining: {}", parser.remaining());
}

Pattern 3: Returning References from Methods

struct Container {
    data: Vec<String>,
}

impl Container {
    fn get(&self, index: usize) -> Option<&String> {
        self.data.get(index)
    }

    fn first(&self) -> Option<&String> {
        self.data.first()
    }
}

fn main() {
    let container = Container {
        data: vec![String::from("a"), String::from("b")],
    };

    if let Some(first) = container.first() {
        println!("First: {}", first);
    }
}

Practice Exercise

Reading lifetime annotations is not the same as choosing one. This is a build task: a small program that reports its own pass/fail. You are given a LogView — a struct that borrows a block of log text and hands out slices of it without copying a single byte — and three stubbed methods. Run it as-is and it fails immediately, naming the first check that did not pass. Implement each until every check passes and it prints All checks passed.

The interesting part is already done for you and is worth reading before you write anything: every return type is annotated &'a str, the struct's lifetime, not &self's. That is the decision the debug block above was about, and the checks are built to depend on it — main deliberately lets the LogView die at a closing brace while the slices it produced live on. Elided signatures would tie those slices to the view and none of it would compile. Your job is only the three bodies.

Build

Finish the build. Three methods are stubbed out and the checks below them fail until each returns the right slice. Run it as-is to see which check fails first, decide what that method is missing, then implement all three until it prints 'All checks passed.' The checks run top to bottom, so the first failure you see is TODO 1 — implement it first, then work down.

// A log scanner that hands out slices of the text it was built from.
// It never copies a single byte: every &str it returns points into text.
//
// Every return type below is already annotated 'a — the STRUCT's lifetime, not
// &self's. That is the one decision this exercise turns on: elision would have
// tied these to &self, and the checks below would then refuse to compile.

struct LogView<'a> {
  text: &'a str,
}

impl<'a> LogView<'a> {
  fn new(text: &'a str) -> LogView<'a> {
      LogView { text }
  }

  // TODO 1: return the FIRST line of self.text, or "" if there are none.
  //   self.text.lines() yields each line as a &str borrowed from the text.
  fn first_line(&self) -> &'a str {
      let _ = self.text;
      ""
  }

  // TODO 2: return the first line that CONTAINS needle, or None.
  //   Note that needle is deliberately NOT 'a — the answer is borrowed
  //   from self.text, never from the string you searched with.
  fn find_line(&self, needle: &str) -> Option<&'a str> {
      let _ = needle;
      None
  }

  // TODO 3: return the LONGEST line in self.text, or "" if there are none.
  fn longest(&self) -> &'a str {
      ""
  }
}

// --- Build checks: these must all pass. Do not edit below this line. ---
fn main() {
  let log = String::from("INFO boot ok
ERROR disk full and getting fuller
WARN slow");

  // Every borrowed slice must outlive the LogView it came from.
  let first;
  let found;
  let longest;
  {
      let view = LogView::new(&log);
      first = view.first_line();
      found = view.find_line("ERROR");
      longest = view.longest();
  } // view is gone here — the slices are not.

  assert_eq!(first, "INFO boot ok", "first_line should return the first line");
  assert_eq!(
      found,
      Some("ERROR disk full and getting fuller"),
      "find_line should return the first line containing the needle"
  );
  assert_eq!(
      LogView::new(&log).find_line("nothing here"),
      None,
      "find_line should return None when no line matches"
  );
  assert_eq!(
      longest, "ERROR disk full and getting fuller",
      "longest should return the longest line"
  );

  println!("All checks passed.");
  println!("first: {}", first);
  println!("found: {}", found.unwrap());
  println!("longest: {}", longest);
}

Expected output: All checks passed. first: INFO boot ok found: ERROR disk full and getting fuller longest: ERROR disk full and getting fuller

Once it passes, try two variations and predict each before running:

  1. Delete one 'a. Change fn longest(&self) -> &'a str to fn longest(&self) -> &str and predict which line the compiler will point at before running. Your body is still perfectly correct, but the program stops compiling: error[E0597]: view does not live long enough, pointing at the call and then at the closing brace where view is dropped "while still borrowed". Elision has now tied the returned slice to the LogView that dies there. Nothing about the data changed; only the promise in the signature did.
  2. Annotate the wrong thing. In find_line, change the signature to fn find_line<'n>(&self, needle: &'n str) -> Option<&'n str> and decide what the compiler will say before running. It rejects the body, not the call: the lines you are returning come from self.text, and nothing lets the compiler believe they live as long as needle. A lifetime annotation is a claim about where a reference came from, so pointing it at the wrong source is caught immediately.

Key Takeaways

  • Lifetimes ensure references are always valid
  • Most lifetimes are inferred by the compiler
  • Use 'a syntax when the compiler needs help
  • Lifetime annotations describe relationships, they don't change how long things live
  • Structs holding references need lifetime parameters
  • 'static means "lives for the entire program"
  • Lifetime elision rules reduce annotation boilerplate

Lifetimes are one of Rust's most powerful features for memory safety!

Next Steps

With lifetimes understood, you're ready for traits and generics — Rust's tools for writing flexible, reusable code that works across different types.

Next lesson

Traits & Generics

Learn to write flexible, reusable code with Rust's trait system and generic programming

30 min