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 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.
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:
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:
// 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.
// 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:
// 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.
- Each input reference gets its own lifetime parameter
- If exactly one input lifetime exists, it applies to all output references
- If
&selfor&mut selfexists, its lifetime applies to all output references
These rules handle most common patterns:
// 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:
// 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 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 scopeThe '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:
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:
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:
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:
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:
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 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:
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?
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:
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?
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?
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 Tis covariant in'a: a longer lifetime can substitute for a shorter one&'a mut Tis invariant inT: the type must match exactlyfn(&'a T)is contravariant in'a: a shorter lifetime can substitute for a longer one
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
'staticbound 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.
Can you spot the bug in Rust?
One real snippet, one hidden bug, one attempt a day. No account needed to try.

Written by
Anthony Fillion-MailletFounder of SharpSkill
Full-stack developer for over 10 years. Runs SharpSkill and answers for everything published here.
Updated on August 27, 2026
Tags
Share
Related articles

Rust and SQLx in 2026: Compile-Time Checked Queries and Interview Questions
Master SQLx 0.9 for Rust with compile-time checked queries, migrations, and the new sqlx.toml configuration. Includes real interview questions and production patterns.

Rust Smart Pointers Explained: Box, Rc, Arc and RefCell in 2026
Rust smart pointers Box, Rc, Arc and RefCell explained with compilable 2026 examples, a decision table and common interview questions.

Rust Traits and Generics in 2026: Trait Upcasting, AsyncFn and Advanced Patterns
Master Rust traits and generics with the latest 2024 Edition features: trait upcasting, AsyncFn closures, RPITIT, and advanced patterns tested in real interviews.