Rust Lifetimes Explained: Annotations, Elision and Interview Questions 2026

Master Rust lifetimes with this deep-dive covering lifetime annotations, elision rules, and common interview questions. Learn how the borrow checker enforces memory safety.

Rust lifetimes visualization showing memory ownership and borrowing concepts

Rust lifetimes are the compiler's mechanism for tracking how long references remain valid. Unlike garbage-collected languages where memory management happens at runtime, Rust's borrow checker validates reference validity at compile time, eliminating entire categories of memory bugs before the code ever runs.

Interview Quick Answer

A lifetime in Rust is a compile-time construct that describes the scope during which a reference is valid. The borrow checker uses lifetimes to ensure references never outlive the data they point to, preventing dangling pointers without runtime overhead.

What Lifetimes Actually Represent in Memory

Lifetimes are not about how long values exist. They describe how long references to values remain valid. Every reference in Rust has a lifetime, even when annotations are omitted.

Consider this function that the compiler rejects:

dangling_reference.rsrust
fn create_dangling() -> &String {
    let s = String::from("hello");
    &s  // ERROR: `s` is dropped at end of function
}

The String s lives only within the function scope. Returning a reference to it would create a dangling pointer, since the String's memory gets deallocated when the function returns. The borrow checker catches this at compile time.

The fix requires either returning an owned value or ensuring the referenced data outlives the function call:

valid_return.rsrust
// Option 1: Return owned value
fn create_owned() -> String {
    String::from("hello")
}

// Option 2: Reference data that outlives the function
fn first_word(s: &str) -> &str {
    s.split_whitespace().next().unwrap_or("")
}

In first_word, the returned &str borrows from the input s, so it remains valid as long as s does. The caller controls the input's lifetime.

Lifetime Annotation Syntax and Semantics

Explicit lifetime annotations use the syntax 'a, 'b, and so on. These are not instructions to the compiler about how long references should live. They describe relationships between the lifetimes of multiple references.

The Rust Reference defines lifetime annotations as generic parameters that constrain how long references must remain valid relative to each other.

lifetime_annotations.rsrust
// Both inputs and output share the same 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 result;
    {
        let string2 = String::from("short");
        result = longest(&string1, &string2);
        println!("Longest: {}", result);  // Valid: both strings alive
    }
    // println!("{}", result);  // ERROR: string2 dropped
}

The annotation 'a tells the compiler: the returned reference will be valid for the intersection of the lifetimes of x and y. Since string2 has a shorter lifetime, result cannot be used after string2 drops.

Multiple distinct lifetimes express more complex relationships:

multiple_lifetimes.rsrust
// Output tied only to first parameter's lifetime
fn first_only<'a, 'b>(x: &'a str, _y: &'b str) -> &'a str {
    x
}

fn main() {
    let owned = String::from("owned");
    let result;
    {
        let temporary = String::from("temporary");
        result = first_only(&owned, &temporary);
    }
    // result still valid: only depends on owned
    println!("{}", result);
}

Lifetime Elision Rules in Rust 2024

The compiler applies three elision rules to infer lifetimes when annotations are omitted. These rules, documented in the Rustonomicon, reduce boilerplate without sacrificing safety.

The Three Elision Rules
  1. Each input reference gets its own lifetime parameter
  2. If exactly one input lifetime exists, it applies to all output references
  3. If &self or &mut self exists, its lifetime applies to all output references

These rules handle most common patterns:

elision_examples.rsrust
// Rule 1: Each input gets own lifetime
fn takes_two(x: &str, y: &str) {}
// Compiler reads: fn takes_two<'a, 'b>(x: &'a str, y: &'b str) {}

// Rule 2: Single input lifetime propagates to output  
fn first_char(s: &str) -> &str {
    &s[0..1]
}
// Compiler reads: fn first_char<'a>(s: &'a str) -> &'a str

// Rule 3: &self lifetime propagates to output
impl Parser {
    fn peek(&self) -> &Token {
        &self.tokens[self.position]
    }
    // Compiler reads: fn peek<'a>(&'a self) -> &'a Token
}

When elision fails, the compiler requires explicit annotations. This happens most often with functions returning references derived from multiple inputs:

elision_fails.rsrust
// ERROR: Can't determine output lifetime
fn ambiguous(x: &str, y: &str) -> &str {
    if x.len() > y.len() { x } else { y }
}

// FIX: Explicit annotation resolves ambiguity
fn unambiguous<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

Struct Lifetimes and the Outlives Relationship

Structs containing references require lifetime annotations to express that borrowed data must outlive the struct:

struct_lifetimes.rsrust
struct Excerpt<'a> {
    text: &'a str,
}

impl<'a> Excerpt<'a> {
    // new borrows from input, so Excerpt can't outlive the source
    fn new(source: &'a str, start: usize, end: usize) -> Self {
        Excerpt { text: &source[start..end] }
    }
    
    // level() returns owned data, no lifetime in signature
    fn level(&self) -> u32 {
        self.text.len() as u32 / 10
    }
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let excerpt = Excerpt::new(&novel, 0, 16);
    println!("Excerpt: {}", excerpt.text);
}  // novel dropped, but excerpt already out of scope

The 'a in Excerpt<'a> binds the struct's validity to the lifetime of the borrowed text. Attempting to use an Excerpt after its source String drops triggers a compile error.

For structs with multiple references, each may have distinct lifetimes:

multiple_struct_lifetimes.rsrust
struct Comparison<'a, 'b> {
    baseline: &'a str,
    candidate: &'b str,
}

impl<'a, 'b> Comparison<'a, 'b> {
    fn new(baseline: &'a str, candidate: &'b str) -> Self {
        Comparison { baseline, candidate }
    }
    
    fn baseline_only(&self) -> &'a str {
        self.baseline
    }
}

Ready to ace your Rust interviews?

Practice with our interactive simulators, flashcards, and technical tests.

The 'static Lifetime and When to Use It

The 'static lifetime indicates data that lives for the entire program execution. String literals have this lifetime because they're embedded in the binary:

static_lifetime.rsrust
let s: &'static str = "I live forever";

// Constants are implicitly 'static
const CONFIG_VERSION: &str = "2.0.0";

// Thread-safe globals require 'static
static COUNTER: AtomicU64 = AtomicU64::new(0);

A common pattern involves T: 'static bounds, which often confuses developers. This bound means T contains no non-static references, not that T must be a reference:

static_bound.rsrust
use std::thread;

fn spawn_task<T: Send + 'static>(data: T) {
    thread::spawn(move || {
        // data moved into thread, must live independently
        println!("Processing in thread");
    });
}

fn main() {
    let owned = String::from("owned data");
    spawn_task(owned);  // OK: String is 'static (owns its data)
    
    let reference = "borrowed";
    // spawn_task(reference);  // OK: &'static str
}

The thread might outlive the spawning function, so data moved into threads must not contain references to stack-local variables.

Common Lifetime Errors and Their Solutions

Several patterns consistently trip up developers. Understanding these accelerates debugging.

Returning references to local variables:

error_local_ref.rsrust
fn bad_split(text: &str, delimiter: char) -> (&str, &str) {
    let parts: Vec<&str> = text.split(delimiter).collect();
    (parts[0], parts[1])  // OK: parts[i] borrow from text, not from Vec
}

fn truly_bad() -> &str {
    let local = String::from("local");
    &local  // ERROR: local dropped at end of function
}

Storing references in structs with mismatched lifetimes:

error_struct_lifetime.rsrust
struct Cache {
    data: String,
    // view: &str,  // ERROR: needs lifetime parameter
}

// Self-referential structs require unsafe or crates like ouroboros
struct CacheWithView<'a> {
    data: String,
    view: Option<&'a str>,  // Can't point to self.data safely
}

Self-referential structs, where a field references another field, require either Pin with unsafe code or crates like ouroboros. The Rust smart pointers module covers these advanced patterns.

Lifetime bounds on trait implementations:

trait_lifetime_bounds.rsrust
trait Processor {
    fn process<'a>(&self, input: &'a str) -> &'a str;
}

struct Prefixer {
    prefix: String,
}

impl Processor for Prefixer {
    // Can't return &format!(...) - would be temporary
    fn process<'a>(&self, input: &'a str) -> &'a str {
        input  // Must return input or part of it
    }
}

Higher-Ranked Trait Bounds (HRTBs) for Generic Lifetimes

Higher-ranked trait bounds use the for<'a> syntax to express that a type must satisfy a trait for any lifetime, not just a specific one:

hrtb.rsrust
use std::fmt::Debug;

// F must be callable with any lifetime 'a
fn apply_to_refs<F>(f: F)
where
    F: for<'a> Fn(&'a str) -> &'a str,
{
    let owned = String::from("test");
    let result = f(&owned);
    println!("{}", result);
}

fn identity(s: &str) -> &str { s }

fn main() {
    apply_to_refs(identity);
}

HRTBs appear frequently in closure-accepting APIs and the Rust async/await ecosystem where futures must work with references of varying lifetimes.

Interview Questions on Rust Lifetimes

Technical interviews probe understanding of lifetimes at multiple depths. These questions appear regularly in Rust positions.

Question: Why does this code fail to compile?

interview_q1.rsrust
fn get_str() -> &str {
    "hello"
}

Answer: The return type needs an explicit lifetime annotation. While the string literal has 'static lifetime, the function signature doesn't express this. The fix: fn get_str() -> &'static str.

Question: Explain why this compiles and whether it's safe:

interview_q2.rsrust
fn longest<'a>(x: &'a str, _y: &str) -> &'a str {
    x
}

Answer: This compiles because the output lifetime is tied only to x. The _y parameter can have any lifetime since the return value doesn't depend on it. This is safe: the returned reference's validity depends solely on x's lifetime.

Question: What happens when you try to store a reference alongside owned data?

interview_q3.rsrust
struct Config<'a> {
    name: String,
    description: &'a str,
}

Answer: This pattern is valid but constrains how Config can be used. The struct can't outlive whatever description references. For owned data that needs to reference itself, consider using String for both fields, or techniques covered in Rust ownership and borrowing.

Question: How do lifetimes interact with trait objects?

interview_q4.rsrust
trait Formatter {
    fn format(&self, input: &str) -> String;
}

fn get_formatter<'a>() -> Box<dyn Formatter + 'a> {
    // ...
}

Answer: Trait objects have an implicit 'static lifetime bound by default. Writing Box<dyn Formatter + 'a> explicitly allows the trait object to contain references with lifetime 'a. Without the explicit bound, Box<dyn Formatter> equals Box<dyn Formatter + 'static>.

Lifetime Variance: Covariance and Contravariance

Variance determines how lifetimes relate when types are nested. Rust references follow these rules:

  • &'a T is covariant in 'a: a longer lifetime can substitute for a shorter one
  • &'a mut T is invariant in T: the type must match exactly
  • fn(&'a T) is contravariant in 'a: a shorter lifetime can substitute for a longer one
variance.rsrust
fn covariant_example() {
    let s: &'static str = "static";
    let r: &str = s;  // OK: 'static lives longer than any 'a
}

fn invariant_example() {
    let mut vec: Vec<&'static str> = vec!["a"];
    // let s = String::from("local");
    // vec.push(&s);  // ERROR: &s is not &'static str
}

Understanding variance matters when designing generic APIs that accept or return references. The Rust traits and generics article covers advanced generic patterns.

Start practicing!

Test your knowledge with our interview simulators and technical tests.

Lifetime Annotations in Practice: Key Takeaways

  • Lifetimes describe reference validity, not value existence. The borrow checker uses them to prevent dangling pointers at compile time.
  • Elision rules handle most cases automatically. Explicit annotations become necessary when returning references derived from multiple inputs.
  • Struct lifetimes express the outlives relationship: any struct containing references must be parameterized by those references' lifetimes.
  • The 'static bound on generics means "contains no non-static references," not "must be a reference."
  • Self-referential structs require special handling through Pin, unsafe code, or helper crates.
  • Interview questions focus on understanding why code fails to compile and how lifetime annotations change validity guarantees.
Daily challenge

Can you spot the bug in Rust?

One real snippet, one hidden bug, one attempt a day. No account needed to try.

Anthony Fillion-Maillet

Written by

Anthony Fillion-Maillet

Founder of SharpSkill

Full-stack developer for over 10 years. Runs SharpSkill and answers for everything published here.

Updated on August 27, 2026

Tags

#rust
#lifetimes
#borrow-checker
#memory-safety
#interview

Share

Related articles