# 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. - Published: 2026-08-27 - Updated: 2026-08-27 - Author: Anthony Fillion-Maillet - Tags: rust, lifetimes, borrow-checker, memory-safety, interview - Reading time: 12 min --- 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: ```rust // dangling_reference.rs 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: ```rust // valid_return.rs // 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](https://doc.rust-lang.org/reference/trait-bounds.html#lifetime-bounds) defines lifetime annotations as generic parameters that constrain how long references must remain valid relative to each other. ```rust // lifetime_annotations.rs // 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: ```rust // multiple_lifetimes.rs // 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](https://doc.rust-lang.org/nomicon/lifetime-elision.html), 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: ```rust // elision_examples.rs // 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: ```rust // elision_fails.rs // 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: ```rust // struct_lifetimes.rs 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: ```rust // multiple_struct_lifetimes.rs 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 } } ``` ## 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: ```rust // static_lifetime.rs 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: ```rust // static_bound.rs use std::thread; fn spawn_task(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:** ```rust // error_local_ref.rs 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:** ```rust // error_struct_lifetime.rs 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](https://docs.rs/ouroboros/). The [Rust smart pointers](/technologies/rust/interview-questions/smart-pointers) module covers these advanced patterns. **Lifetime bounds on trait implementations:** ```rust // trait_lifetime_bounds.rs 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: ```rust // hrtb.rs use std::fmt::Debug; // F must be callable with any lifetime 'a fn apply_to_refs(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](/blog/rust/rust-async-await-tokio-futures-concurrency) 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?** ```rust // interview_q1.rs 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:** ```rust // interview_q2.rs 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?** ```rust // interview_q3.rs 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](/blog/rust/rust-ownership-borrowing-demystified). **Question: How do lifetimes interact with trait objects?** ```rust // interview_q4.rs trait Formatter { fn format(&self, input: &str) -> String; } fn get_formatter<'a>() -> Box { // ... } ``` **Answer:** Trait objects have an implicit `'static` lifetime bound by default. Writing `Box` explicitly allows the trait object to contain references with lifetime `'a`. Without the explicit bound, `Box` equals `Box`. ## 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 ```rust // variance.rs 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](/blog/rust/rust-traits-generics-advanced-guide) article covers advanced generic patterns. ## 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. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/en/blog/rust/rust-lifetimes-explained-annotations-elision-interview