Rust’s borrow checker must ensure that every reference is valid for its entire use. Without lifetime Annotations, the compiler cannot prove that a reference outlives the scope in which it is used. This Prevents dangling references — references to memory that has been freed or invalidated.
Every reference in Rust has a lifetime — a region of code during which the reference is valid. In Most cases, the compiler infers lifetimes automatically. Explicit annotations are needed when the Relationship between input and output lifetimes is ambiguous.
Lifetimes use a leading apostrophe followed by a name. By convention, 'a is the first lifetime, 'b the second, and so on. The name is purely a compile-time label — it has no runtime Representation.
The relationship between input and output lifetimes determines how references flow through a Function:
Reference. Adding `'static` constraints reduces the function's flexibility — callers can no longer Pass locally-owned string slices. The compiler may suggest `'static` when it cannot infer a shorter Lifetime, but this is often a sign that the function signature needs redesign.fn print (s : & ' static str ) {
When a struct holds a reference, you must annotate the reference’s lifetime:
let novel = String :: from ( "Call me Ishmael. Some years ago..." );
let words = novel . as_str ();
let i = words . find ( '.' ) . unwrap ();
first_sentence = Excerpt { part : & words[ .. i] };
// first_sentence is valid here because its lifetime is bounded by novel's lifetime
let s1 = String :: from ( "hello" );
let s2 = String :: from ( "world" );
When a struct has a lifetime parameter and also holds a type that references that lifetime, you may Need a lifetime bound:
struct Container <' a , T : "a> {
The bound T: "a says “T must outlive 'a.” This is automatically added by the Compiler, but you may need to write it explicitly for complex generic constraints.
When implementing methods on a struct with a lifetime parameter, the compiler automatically assigns &self’s lifetime to all output lifetime parameters (elision rule 3). This means you rarely need Explicit lifetime annotations on methods:
// No explicit lifetime needed — elision rule 3 applies
// Return lifetime is tied to self's lifetime
fn announce_and_return ( & self , announcement : & str ) -> & str {
println! ( "attention: {}" , announcement);
// Multiple lifetimes — explicit annotations needed
fn compare <' b >( & self , other : & ' b str ) -> bool {
self . part . len () == other . len ()
Static methods (no self parameter) require explicit lifetime annotations:
fn from_parts (part : & ' a str ) -> Self {
The compiler applies three rules to infer lifetimes. If after applying all three rules, the compiler Still cannot determine lifetimes, it errors.
Every parameter that is a reference gets its own lifetime parameter:
fn foo (x : & str ) → fn foo<' a >(x : & ' a str )
fn foo (x : & str , y : & str ) → fn foo<' a , ' b >(x : & ' a str , y : & ' b str )
fn foo (x : & i32 , y : &mut i32 ) → fn foo<' a , ' b >(x : & ' a i32 , y : & ' b mut i32 )
If there is exactly one input lifetime parameter, that lifetime is assigned to all output Parameters:
fn foo (x : & str ) -> & str → fn foo<' a >(x : & ' a str ) -> & ' a str
fn foo (x : & str ) -> ( & str , & str ) → fn foo<' a >(x : & ' a str ) -> ( & ' a str , & ' a str )
If there are multiple input lifetime parameters but one of them is &self or &mut selfThe Lifetime of self is assigned to all output parameters:
fn method ( & self , x : & str ) -> & str → fn method<' a , ' b >( & ' a self , x : & ' b str ) -> & ' a str
fn method ( &mut self ) -> & str → fn method<' a >( & ' a mut self ) -> & ' a str
// This does NOT compile — two input lifetimes, no self, ambiguous output
fn merge (x : & str , y : & str ) -> & str {
if x . len () > y . len () { x } else { y }
// Fix: add explicit lifetime annotations
fn merge <' a >(x : & ' a str , y : & ' a str ) -> & ' a str {
if x . len () > y . len () { x } else { y }
Lifetimes can have bounds, just like type parameters. The syntax 'a: "b means “a outlives b” — the Reference with lifetime ''a must live at least as long as "b:
fn print <' a , ' b : "a>(x: &''b str, y: &" a str ) {
This is useful when you need to ensure that one reference is valid for at least as long as another. It is common in structs that hold references with different lifetimes:
struct Context <' a , ' b : "a> {
fn process ( & self ) -> Self :: Output ;
fn run <' a , T >(processor : & ' a dyn Processor < Output = T >) -> T
The T: ''a bound ensures that T does not contain references shorter than "a. This is necessary Because the trait object might reference data with lifetime 'a.
Variance determines whether a longer lifetime can be substituted for a shorter one. This is critical For writing correct generic code.
&'a T is covariant in 'a. If 'long: "shortThen &''long T can be used where &"short T is Expected. This is safe because a longer-lived reference is a subtype of a shorter-lived one when you Only read through it:
fn takes_short <' a >(r : & ' a str ) {}
let long : & ' static str = "hello" ;
takes_short (long); // OK — 'static can be shortened to 'a
Function types are contravariant in their argument lifetimes. If 'short: "longThen a function Expecting a ''long reference can be used where a function expecting a "short reference is needed:
fn apply <' a , F >(f : F , arg : & ' a str )
&mut T is invariant in both 'a and T. You cannot substitute a &'long mut T where a &'short mut T is expected. This prevents soundness issues where a mutable reference to a Shorter-lived value could be used to write a longer-lived reference, extending its validity beyond Its scope:
fn takes_short_mut <' a >(r : & ' a mut i32 ) {
let r : & ' static mut i32 = unsafe { &mut * Box :: into_raw ( Box :: new (x)) };
// takes_short_mut(r); // ERROR — &'static mut i32 is invariant
Type Variance in 'a Variance in T &'a TCovariant Covariant &'a mut TInvariant Invariant Box<T>— Covariant Cell<T>— Invariant fn(&'a T) -> RContravariant Contravariant fn(T) -> &'a RCovariant Covariant
Violating variance assumptions in unsafe code causes undefined behavior. If you store a &'long T In a position that the compiler believes holds a &'short TThe short reference may be used after The long reference’s referent is freed:
fn variance_bug <' a , ' b : "a>(holder: &Holder<''b>) {
let short: &" a str = & String :: from ( "short-lived" );
// This is SOUND because Cell is invariant in T.
// If Holder used a covariant type, this would be unsound.
HRTBs express constraints on lifetimes that are universally quantified. The syntax for<'a> means “for all lifetimes ‘a”:
F : for <' a > Fn ( & ' a str ) -> & ' a str ,
let s = String :: from ( "hello" );
The closure must work with any lifetime 'aNot just a specific one. This is more restrictive than Specifying a single lifetime because the closure cannot capture references with a specific lifetime.
// A function that accepts any closure that works with any string lifetime
fn call_with_any_str < F : for <' a > Fn ( & ' a str ) -> bool >(f : F ) {
let s1 = String :: from ( "hello" );
HRTBs appear most commonly in trait bounds for functions that accept callbacks:
fn map_values < K , V1 , V2 , F >(map : & HashMap < K , V1 >, f : F ) -> HashMap < K , V2 >
map . iter () . map ( | (k, v) | (k . clone (), f (v))) . collect ()
Lifetime subtyping means 'long: "short (long outlives short). A longer lifetime is a subtype of a Shorter one. This is used implicitly by the compiler when checking borrow validity:
let r1 : & '' static i32 = & x; // ERROR: x does not live for "static
let r2 : & ' _ i32 = & x; // OK: compiler infers an appropriate lifetime
The compiler performs subtyping during borrow checking. If a function expects &'a T and you pass &'b T where 'b: "aThe compiler accepts it because a longer-lived reference satisfies a Shorter-lived requirement.
Rust cannot express structs that hold references to their own fields in safe code. The struct and Its field share the same lifetime, but the borrow checker treats them as independent:
// This does NOT compile:
pointer : & str , // what lifetime?
Solutions include:
Index-based approach (most common):
Arena allocation (via crates like bumpalo or typed-arena):
let a = arena . alloc ( "hello" );
let b = arena . alloc ( "world" );
// a and b have the same lifetime — cross-references are valid
Pin-based approach for self-referential async state machines:
use std :: marker :: PhantomPinned ;
fn new (data : String ) -> Self {
pointer : std :: ptr :: null (),
fn init ( self : Pin < &mut Self >) {
let self_ptr : *const String = & self . data;
// SAFETY: the struct is pinned, so data will not move
let this = self . get_unchecked_mut ();
The safest pattern is to have the output lifetime match exactly one input lifetime:
fn first_word < '' a>(text : & "a str) -> &'a str {
fn transform ( & self , input : & ' a str ) -> Self :: Output ;
impl <' a > Transformer <' a > for ToUpper {
fn transform ( & self , input : & ' a str ) -> String {
Lifetimes have zero runtime cost. They are purely compile-time annotations. The compiler erases all Lifetime information before code generation. A &'a T and a &'b T produce identical machine code — the lifetimes exist only for the borrow checker’s verification.
Enums that hold references also require lifetime annotations:
let s = StrOrInt :: Str ( "hello" );
let n = StrOrInt :: Int ( 42 );
StrOrInt :: Str (text) => println! ( "text: {}" , text),
StrOrInt :: Int (val) => println! ( "number: {}" , val),
The borrow checker also verifies that structs are dropped before the data they reference. The drop Checker can be overly conservative:
// The compiler must ensure that Context is dropped before data
// This is automatic for simple cases
If your struct contains a raw pointer that does not actually reference the lifetime parameter, you Can use #[may_dangle] (unsafe) to relax the drop check. This is advanced and should be used only When you can prove safety manually.
When using impl Trait in return position, lifetimes are inferred:
fn get_parts (s : & str ) -> impl Iterator < Item = & str > {
The compiler infers the appropriate lifetime for the returned iterator. If the inference is Ambiguous, you may need to write it explicitly:
fn get_parts <' a >(s : & ' a str ) -> impl Iterator < Item = & ' a str > + ' a {
Over-annotating with 'static. The compiler suggests 'static when it cannot infer a shorter lifetime. Adding 'static often reduces API flexibility. Instead, redesign the function to take an explicit lifetime parameter or restructure ownership.
Confusing lifetime names with actual lifetimes. 'a and 'b are just labels. Two functions using 'a in their signatures do not share a lifetime — the compiler resolves each independently at each call site.
Fighting the borrow checker with clones. Cloning to satisfy lifetime constraints often indicates a design issue. Consider whether you can restructure ownership, use indices instead of references, or redesign the data flow.
Not understanding variance. Misunderstanding covariance and invariance leads to subtle soundness bugs in generic code. If you are writing unsafe code that involves lifetimes, verify variance carefully.
Self-referential structs without Pin. Attempting to create a struct that references its own fields will not compile in safe Rust. Use indices, arena allocation, or Pin for self-referential patterns.
Lifetime elision hiding complexity. Elision rules make simple cases ergonomic, but can obscure lifetime relationships in complex functions. When debugging lifetime errors, write out the fully explicit lifetimes to understand what the compiler is doing.
Ignoring the T: "a bound. When a generic type T might contain references, the compiler may require T: ''a to ensure that T does not contain references shorter than "a. This is especially common with trait objects and Box<dyn Trait>.
Assuming lifetimes affect runtime. Lifetimes are erased at compile time. They have zero runtime cost. A reference with a 'static lifetime is not “better” or “more efficient” than one with a shorter lifetime.
Using unsafe to extend lifetimes. Transmuting a shorter-lived reference to a longer-lived one is undefined behavior. No amount of unsafe can make a dangling reference valid. If the borrow checker rejects your code, the solution is restructuring, not bypassing.
Multiple lifetime parameters when one suffices. If all references in a function can share a single lifetime, use one lifetime parameter. Multiple lifetime parameters are needed only when references have genuinely independent lifetimes.
Closures can capture references, and the compiler infers lifetimes for those captures. However, the Lifetime rules for closures are different from functions because closures capture their environment:
fn make_closure <' a >() -> Box < dyn Fn ( & ' a str ) -> usize > {
Box :: new ( | s : & str | s . len ())
The closure’s return type is inferred from its body. When stored in a trait object, the lifetime Must be explicitly specified. This is because the trait object has an implicit lifetime bound:
fn make_closure <' a >() -> Box < dyn Fn ( & ' a str ) -> usize + ' a > {
Box :: new ( | s : & str | s . len ())
When a closure captures a reference, the closure’s type carries a lifetime bound. This affects where The closure can be stored and used:
fn with_callback <' a , F >(data : & ' a str , callback : F )
let s = String :: from ( "hello" );
with_callback ( & s, | data | {
println! ( "callback received: {}" , data);
left : Option < Box < TreeNode <' a >>>,
right : Option < Box < TreeNode <' a >>>,
fn build_tree <' a >(values : & ' a [ & ' a str ]) -> Option < Box < TreeNode <' a >>> {
let mid = values . len () / 2 ;
left : build_tree ( & values[ .. mid]),
right : build_tree ( & values[mid + 1 .. ]),
let data = vec! [ "alpha" , "bravo" , "charlie" , "delta" , "echo" ];
let tree = build_tree ( & data);
Linked lists in Rust are notoriously difficult because each node references the next node. The Simplest approach uses indices instead of references:
fn push_front ( &mut self , value : T ) {
let new_index = self . nodes . len ();
let old_head = self . head;
self . nodes . push ( Node { value, next : old_head });
self . head = Some (new_index);
The bound T: ''a means “T does not contain any references with a lifetime shorter than “a.” This is Automatically added by the compiler but may be needed explicitly:
// The compiler adds T: "a automatically here
fn get_wrapper < '' a, T : "a>(data: &'a T) -> Wrapper<'a, T> {
When T contains references, the compiler must ensure those references are valid for the lifetime 'a. Without T: "aThe compiler cannot verify this:
struct RefWrapper < '' a, T : "a> {
// This works because String has no references (String: " static )
let s = String :: from ( "hello" );
let w = RefWrapper { inner : & s };
// This also works — the lifetime of the reference is shorter than ''a
let w = RefWrapper { inner : r };
fn serialize < "a, T>(value: &'a T) -> String
T: " a + std :: fmt :: Display ,
Deserializers that return borrowed data require lifetime annotations on the output type:
struct SimpleParser < '' a> {
impl<'a> SimpleParser<'a> {
fn new(input: &'a str) -> Self {
SimpleParser { input, pos: 0 }
fn next_word(&mut self) -> Option<&'a str> {
let remaining = &self.input[self.pos..];
let end = remaining.find(char::is_whitespace)?;
let word = &remaining[..end];
The returned &'a str borrows from the parser’s input field, which has lifetime 'a. This means The returned slices are valid as long as the parser’s input is valid — zero-copy parsing.
Return position impl trait in traits (RPITIT) interacts with lifetimes:
type Iter : Iterator < Item = Self :: Item >;
fn iter ( & ' a self ) -> Self :: Iter ;
struct MySlice <' a , T >( & ' a [ T ]);
impl <' a , T : Clone + ' a > Iterable <' a > for MySlice <' a , T > {
type Iter = std :: iter :: Cloned <std :: slice :: Iter <' a , T >>;
fn iter ( & ' a self ) -> Self :: Iter {
fn process <' a >(data : & ' a str ) -> impl Iterator < Item = & ' a str > + ' a {
The + 'a bound on the return type tells the compiler that the returned iterator may contain References with lifetime 'a. Without this bound, the compiler may not be able to infer the correct Lifetime.
Cow (Clone on Write) has a lifetime parameter that determines whether it borrows or owns:
fn process <' a >(input : & ' a str ) -> Cow <' a , str > {
if input . contains ( "bad" ) {
Cow :: Owned (input . replace ( "bad" , "good" ))
let borrowed = process ( "hello world" );
assert! ( matches! (borrowed, Cow :: Borrowed (_)));
let owned = process ( "hello bad world" );
assert! ( matches! (owned, Cow :: Owned (_)));
Cow<'a, str> is either &'a str (borrowed, no allocation) or String (owned, allocated). The Lifetime 'a applies only to the borrowed variant. When the function returns Cow::BorrowedNo Allocation occurs.
Holding a reference across an .await point is an error because the future may be moved or dropped Between yields:
let data = String :: from ( "hello" );
some_async_function () .await ;
println! ( "{}" , slice); // ERROR: data may have been moved
The fix is to ensure the borrowed data outlives the .await:
let data = String :: from ( "hello" );
let len = data . len (); // Copy the value, not a reference
some_async_function () .await ;
println! ( "{}" , len); // OK — len is a usize, not a reference
Async functions that capture references must satisfy 'static lifetime bounds when spawned on Multi-threaded runtimes:
let data = String :: from ( "hello" );
tokio :: spawn ( async move {
println! ( "{}" , borrowed); // ERROR: borrowed may not live long enough
// Fix: move ownership into the spawned task
let data = String :: from ( "hello" );
tokio :: spawn ( async move {
println! ( "{}" , data); // OK — data is moved into the task
graph TD
A[Does the function return a reference?] -->|No| B[No lifetimes needed]
A -->|Yes| C{Does the reference come from a parameter?}
C -->|No, creates new data| D[Return owned type, not reference]
C -->|Yes, from one parameter| E[Tie output to that parameter's lifetime]
C -->|Yes, from multiple parameters| F{Which parameter's lifetime?}
F -->|Same for all| G[Use single lifetime for all]
F -->|Different| H[Use multiple lifetime parameters]
E --> I{Is self a parameter?}
I -->|Yes| J[Elision rule 3 applies, no annotation needed]
I -->|No| K[Write explicit lifetime annotation] This topic covers the core concepts of lifetimes, including underlying theory, practical implementation, and key applications.
Key concepts include:
arrays and linked lists stacks and queues trees (binary, AVL, BST) hash tables graphs and their representations Understanding these concepts thoroughly is essential for both examinations and practical programming, and requires both theoretical knowledge and hands-on practice.
Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.
Lifetimes are Rust’s way of tracking how long references remain valid. Every reference has a lifetime that the compiler infers or you annotate explicitly. Lifetime elision rules reduce boilerplate in common patterns. The ‘static lifetime means a reference lives for the entire program duration. Lifetimes prevent dangling references by ensuring data outlives the pointers that reference it, and they enable safe borrowing across function boundaries without runtime checks.