Post

Rust Notes: Lifetime

A review of Lifetime in Rust.

Rust Notes: Lifetime

Lifetimes in Rust

Lifetimes are one of the most important parts of Rust’s ownership system.

At first, lifetimes can feel strange because Rust introduces syntax such as:

1
&'a str

and:

1
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str

It is tempting to think that 'a represents a duration of time.

It does not.

A lifetime is primarily a relationship between references.

The compiler uses lifetimes to make sure that a reference never outlives the data it refers to.

The most important idea is:

A lifetime describes how long a reference is valid, and lifetime annotations describe relationships between references.


1. Why Do We Need Lifetimes?

Consider:

1
2
3
4
5
6
7
8
9
10
fn main() {
    let r;

    {
        let x = 10;
        r = &x;
    }

    println!("{r}");
}

This is invalid.

Why?

Because x is destroyed at the end of the inner scope:

1
2
3
4
5
6
{
    let x = 10;
    r = &x;
}   ← x is dropped here

println!("{r}");  ← r would point to dead data

Rust prevents this at compile time.

Conceptually:

1
2
3
4
5
6
7
x:
|----------------|
        scope ends

r:
|------------------------|
        would outlive x

Therefore:

1
2
3
reference lifetime > data lifetime
        ↓
      invalid

Rust rejects the program.


2. Lifetimes Are About References

Suppose:

1
2
3
let x = String::from("hello");

let r = &x;

There are two things involved:

1
2
3
4
5
x
└── owns String data

r
└── borrows x

The lifetime is associated with the reference:

1
r: &'a String

It describes the period during which r is valid as a reference to x.

It does not mean that x itself has an 'a lifetime.


3. Lifetime Is Not a Runtime Timer

This is one of the most important things to understand.

A lifetime is not:

1
2
3
5 seconds
10 seconds
100 milliseconds

Rust does not insert a timer into your program.

Instead, lifetimes are mainly a compile-time concept.

For example:

1
2
3
4
let x = String::from("hello");
let r = &x;

println!("{r}");

The compiler analyzes the scopes and determines that:

1
x is alive while r is used

Therefore the reference is valid.


4. Lifetime as a Region of Validity

It is useful to imagine a lifetime as a region in the program where a reference is valid.

For example:

1
2
3
4
5
6
let x = String::from("hello");

{
    let r = &x;
    println!("{r}");
}

Conceptually:

1
2
3
4
5
x lifetime:
|-----------------------------|

r lifetime:
      |-------------|

The lifetime of r is shorter than the lifetime of x.

That is perfectly valid.

The important rule is:

A reference cannot be used outside the lifetime of the value it refers to.


5. The Borrow Checker Checks Lifetimes

The borrow checker uses ownership, borrowing, and lifetime analysis together.

For example:

1
2
3
4
5
6
7
fn main() {
    let x = String::from("hello");

    let r = &x;

    println!("{r}");
}

The compiler knows:

1
2
3
x owns the String
r borrows x
r must not outlive x

Because x remains alive while r is used, the program is valid.


6. The Most Important Example

Consider:

1
2
3
4
5
6
7
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

You might expect this to work.

But Rust cannot determine the lifetime of the returned reference.

Why?

Because the function can return either:

1
x

or:

1
y

The compiler needs to know the relationship between:

1
2
3
lifetime of x
lifetime of y
lifetime of returned reference

We express that relationship using a lifetime parameter.


7. Lifetime Parameters

The function becomes:

1
2
3
4
5
6
7
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

Here:

1
'a

is a lifetime parameter.

It says:

1
2
3
x is valid for lifetime 'a
y is valid for lifetime 'a
returned reference is valid for lifetime 'a

The important relationship is:

1
2
3
4
5
'a
 │
 ├── x: &'a str
 ├── y: &'a str
 └── return: &'a str

This does NOT mean:

x, y, and the return value live for exactly the same amount of time.

Instead, it expresses a relationship between them.


8. What Does 'a Actually Mean?

Consider:

1
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str

A useful way to read it is:

For some lifetime 'a, both input references are valid for 'a, and the returned reference is also valid for 'a.

The compiler chooses an appropriate lifetime when the function is called.

For example:

1
2
3
4
5
6
7
8
9
10
11
let result;

let s1 = String::from("long string");

{
    let s2 = String::from("short");

    result = longest(&s1, &s2);

    println!("{result}");
}

The returned reference cannot remain valid after s2 disappears if the function might return s2.

Therefore the compiler effectively chooses a lifetime constrained by both references.

Conceptually:

1
2
3
4
5
6
7
8
s1:
|-----------------------------|

s2:
       |-------------|

'a:
       |-------------|

The lifetime must fit within the validity of both inputs.


9. Lifetime Parameters Do Not Extend Lifetimes

This is a common misunderstanding.

Writing:

1
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str

does NOT make x and y live longer.

It does not say:

Keep these strings alive for 'a.

Instead, it says:

The returned reference cannot be valid for longer than the lifetime represented by 'a.

The caller still controls how long the actual values live.


10. Lifetime Annotation Syntax

The basic syntax is:

1
'a

A lifetime name starts with an apostrophe:

1
2
3
'a
'b
'c

For example:

1
&'a str

A reference with a mutable borrow:

1
&'a mut String

A lifetime parameter:

1
2
fn foo<'a>() {
}

Multiple lifetime parameters:

1
2
fn foo<'a, 'b>() {
}

11. Lifetime on Immutable References

Syntax:

1
&'a T

Example:

1
2
3
fn print<'a>(value: &'a String) {
    println!("{value}");
}

Here:

1
&'a String

means:

A reference to a String that is valid for lifetime 'a.


12. Lifetime on Mutable References

Syntax:

1
&'a mut T

Example:

1
2
3
fn modify<'a>(value: &'a mut String) {
    value.push_str(" world");
}

Here:

1
&'a mut String

is a mutable reference with lifetime 'a.

Remember that lifetime and mutability are separate concepts:

1
&'a T

means:

1
2
3
immutable reference
+
lifetime 'a

while:

1
&'a mut T

means:

1
2
3
mutable reference
+
lifetime 'a

13. Lifetime Parameters on Functions

Syntax:

1
2
3
fn function<'a>(x: &'a T) -> &'a T {
    x
}

Example:

1
2
3
fn identity<'a>(x: &'a str) -> &'a str {
    x
}

This expresses:

1
2
3
4
5
input lifetime
      ↓
     'a
      ↓
output lifetime

The returned reference is tied to the input reference.


14. Multiple Lifetimes

A function can have multiple lifetime parameters:

1
2
3
fn choose<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
    x
}

Here:

1
2
'a → lifetime of x
'b → lifetime of y

The returned reference is tied specifically to 'a.

This makes sense because the function always returns x.


15. Multiple Lifetimes with Different Relationships

Consider:

1
2
3
4
5
6
fn first<'a, 'b>(
    x: &'a str,
    y: &'b str,
) -> &'a str {
    x
}

The compiler knows:

1
return → x

Therefore:

1
return lifetime → 'a

It does not need to be related to 'b.


16. Lifetime Relationships

You can express relationships between lifetimes.

For example:

1
2
3
4
5
fn foo<'a, 'b>(x: &'a str, y: &'b str)
where
    'a: 'b,
{
}

The syntax:

1
'a: 'b

means:

Lifetime 'a outlives lifetime 'b.

Conceptually:

1
2
3
4
5
'a:
|----------------------|

'b:
     |-----------|

So:

1
'a: 'b

means 'a lasts at least as long as 'b.


17. Lifetime Bounds

A lifetime can also appear in a type bound.

For example:

1
T: 'a

This means:

T contains no references that are invalid before 'a.

It is often easier to think of it as:

T is valid for at least lifetime 'a.

Example:

1
2
fn foo<'a, T: 'a>(value: T) {
}

18. where Lifetime Bounds

The same constraint can be written using where:

1
2
3
4
5
fn foo<'a, T>(value: T)
where
    T: 'a,
{
}

This is often easier to read when there are many bounds.


19. Structs Containing References

A struct can contain a reference.

For example:

1
2
3
struct Important<'a> {
    part: &'a str,
}

The lifetime parameter belongs to the struct:

1
2
3
Important<'a>
       │
       └── part: &'a str

Example:

1
2
3
4
5
let text = String::from("hello");

let important = Important {
    part: &text,
};

The struct cannot outlive text.


20. Why Does the Struct Need a Lifetime?

Consider:

1
2
3
struct Important {
    part: &str,
}

This does not tell Rust how the reference relates to the lifetime of the struct.

Instead:

1
2
3
struct Important<'a> {
    part: &'a str,
}

means:

Important<'a> contains a reference that must remain valid for 'a.

Therefore:

1
2
3
4
5
6
7
8
9
text
│
├── owns String
│
└── must outlive
        ↓
Important<'a>
        │
        └── part: &'a str

21. Multiple References in a Struct

You can have multiple lifetime parameters:

1
2
3
4
struct Pair<'a, 'b> {
    first: &'a str,
    second: &'b str,
}

Here:

1
2
first  → 'a
second → 'b

The two references do not have to have the same lifetime.


22. One Lifetime for Multiple References

You can also use one lifetime:

1
2
3
4
struct Pair<'a> {
    first: &'a str,
    second: &'a str,
}

This expresses a relationship between the two references.

It means both references are valid for 'a.

Again, this does not mean they necessarily have identical real-world lifetimes.


23. Lifetime on impl

If a struct has a lifetime parameter:

1
2
3
struct Important<'a> {
    part: &'a str,
}

its impl usually includes the lifetime:

1
2
3
4
5
impl<'a> Important<'a> {
    fn part(&self) -> &str {
        self.part
    }
}

The two occurrences have different roles:

1
2
3
4
5
6
7
impl<'a>
     ↑
declares lifetime parameter

Important<'a>
         ↑
uses that lifetime parameter

24. Lifetime on Methods

Example:

1
2
3
4
5
impl<'a> Important<'a> {
    fn get_part(&self) -> &'a str {
        self.part
    }
}

Here the returned reference uses the lifetime stored in the struct.

You can also introduce a separate lifetime for the method:

1
2
3
4
5
impl<'a> Important<'a> {
    fn choose<'b>(&self, other: &'b str) -> &'b str {
        other
    }
}

Now:

1
2
'a → struct lifetime
'b → method lifetime

25. Lifetime Elision

You will often see functions without lifetime annotations:

1
2
3
fn first(s: &str) -> &str {
    s
}

You might wonder:

Where is the lifetime?

Rust has lifetime elision rules.

The compiler can infer the lifetime relationship.

Conceptually, the compiler treats the function approximately like:

1
2
3
fn first<'a>(s: &'a str) -> &'a str {
    s
}

Therefore you usually don’t need to write the lifetime explicitly.


26. Lifetime Elision Rule #1

Every reference parameter gets its own lifetime parameter.

For example:

1
2
fn foo(x: &str, y: &str) {
}

is conceptually treated like:

1
2
fn foo<'a, 'b>(x: &'a str, y: &'b str) {
}

Conceptually, not literally in the source code.


27. Lifetime Elision Rule #2

If there is exactly one input lifetime, that lifetime is assigned to all output references.

For example:

1
2
3
fn identity(x: &str) -> &str {
    x
}

is approximately:

1
2
3
fn identity<'a>(x: &'a str) -> &'a str {
    x
}

This is why many simple functions don’t need lifetime annotations.


28. Lifetime Elision and Methods

Methods have an additional rule.

The lifetime of &self or &mut self is assigned to output references.

For example:

1
2
3
4
5
impl User {
    fn name(&self) -> &str {
        &self.name
    }
}

Conceptually:

1
2
3
4
5
impl User {
    fn name<'a>(&'a self) -> &'a str {
        &self.name
    }
}

This is extremely common in Rust.


29. Why longest Needs an Explicit Lifetime

Now compare:

1
2
3
fn first(x: &str) -> &str {
    x
}

with:

1
2
3
4
5
6
7
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

The first has one input reference.

Rust can infer:

1
input → output

The second has two input references:

1
2
x → ?
y → ?

and the output can come from either one.

Therefore Rust needs us to specify the relationship:

1
2
3
4
fn longest<'a>(
    x: &'a str,
    y: &'a str,
) -> &'a str

30. Lifetimes Do Not Determine Which Value Is Returned

This function:

1
2
3
4
fn longest<'a>(
    x: &'a str,
    y: &'a str,
) -> &'a str

does not mean:

Return the value that has lifetime 'a.

It means:

The returned reference is connected to the lifetime represented by 'a.

The actual returned value is determined by:

1
2
3
4
5
if x.len() > y.len() {
    x
} else {
    y
}

31. Lifetime Annotations Do Not Change Runtime Behavior

Adding:

1
'a

does not make your program:

  • slower
  • faster
  • allocate memory
  • create a runtime object
  • keep a variable alive
  • extend a variable’s scope

Lifetimes are primarily used by the compiler.

For example:

1
2
3
fn identity<'a>(x: &'a str) -> &'a str {
    x
}

does not create a runtime 'a object.


32. Lifetime and Ownership

Lifetimes make sense when combined with ownership.

Suppose:

1
2
3
4
fn get_name() -> &str {
    let name = String::from("Amir");
    &name
}

This is invalid.

Why?

1
2
3
4
5
6
7
8
9
name created
   ↓
&name returned
   ↓
function ends
   ↓
name dropped
   ↓
returned reference would point to dead data

Rust rejects it.


33. You Cannot Return a Reference to a Local Variable

This is one of the most common lifetime errors.

Invalid:

1
2
3
4
fn create() -> &String {
    let s = String::from("hello");
    &s
}

The correct solution is usually to return ownership:

1
2
3
fn create() -> String {
    String::from("hello")
}

Then:

1
let s = create();

The caller owns the returned String.

This is an important Rust design principle:

If the created data needs to outlive the function, return ownership instead of a reference to local data.


34. Lifetime vs Ownership

Compare:

1
2
3
fn create() -> String {
    String::from("hello")
}

with:

1
2
3
fn view<'a>(s: &'a String) -> &'a str {
    &s[..]
}

The first transfers ownership:

1
2
3
4
5
function
   │
   └── creates String
          │
          └── ownership returned to caller

The second borrows:

1
2
3
caller owns String
       │
       └── function temporarily borrows it

Lifetimes describe the second relationship.


35. Lifetime vs Scope

These concepts are related but not identical.

Scope is about where a variable name is accessible:

1
2
3
4
{
    let x = 10;
    println!("{x}");
}

The scope of x is the block.

A lifetime describes how long a reference is valid.

For example:

1
2
3
4
5
6
let x = 10;

{
    let r = &x;
    println!("{r}");
}

Here:

1
2
3
4
5
x scope:
|------------------------|

r scope:
      |----------|

The reference r has a shorter lifetime than x.


36. Non-Lexical Lifetimes

Modern Rust uses non-lexical lifetimes (NLL).

This means a borrow can end when the compiler determines that the reference is no longer used, rather than necessarily waiting until the end of the lexical scope.

For example:

1
2
3
4
5
6
7
let mut x = String::from("hello");

let r = &x;

println!("{r}");

x.push_str(" world");

The immutable borrow can end after the last use of r.

Conceptually:

1
2
3
4
5
6
7
x:
|-----------------------------|

r:
     |--------|
              ↑
           last use

After that point, x can be mutably borrowed.

This makes Rust’s borrowing system much more flexible.


37. Lifetime of a Mutable Reference

Consider:

1
2
3
4
5
6
7
let mut x = String::from("hello");

let r = &mut x;

r.push_str(" world");

println!("{r}");

While r is actively borrowed:

1
2
3
x
│
└── exclusively borrowed by r

You cannot simultaneously use x in a conflicting way.

This is related to the rule:

At any point, you can have either one mutable reference or any number of immutable references.

Lifetimes describe how long those borrows are active.


38. Lifetime and Borrowing

Think of a borrow as having a lifetime.

1
2
3
4
5
let x = String::from("hello");

let r = &x;

println!("{r}");

Conceptually:

1
2
3
4
5
6
x owns data
│
├─────────────── lifetime of x
│
└── r borrows x
    └──────── lifetime of borrow

The borrow checker ensures the borrowing rules remain valid throughout the lifetime of the borrow.


39. 'static

One special lifetime is:

1
'static

It means the reference can remain valid for the entire duration of the program.

For example:

1
let s: &'static str = "hello";

String literals have 'static lifetime because they are embedded in the program’s binary.


40. String Literals and 'static

Consider:

1
let s = "hello";

The type is:

1
&'static str

conceptually.

The data exists for the entire program.

Therefore:

1
let s: &'static str = "hello";

is valid.


41. 'static Does Not Always Mean “Lives Forever”

There is another important use:

1
T: 'static

This does not necessarily mean:

T itself lives forever.

It means:

T does not contain references that are shorter-lived than 'static.

For example:

1
String

is 'static in this sense because it owns its data.

This can therefore satisfy:

1
T: 'static

even though a particular String value can be dropped normally.


42. 'static and Owned Types

Consider:

1
2
fn spawn<T: 'static>(value: T) {
}

A String can satisfy this:

1
2
3
let s = String::from("hello");

spawn(s);

because String owns its data.

But this may not:

1
2
3
4
let s = String::from("hello");
let r = &s;

spawn(r);

because r refers to s, which may not live for 'static.

The important distinction is:

1
2
3
4
5
6
7
String
└── owns data
    └── no borrowed data

&String
└── borrows data
    └── lifetime matters

43. Why Thread APIs Often Require 'static

You will often see:

1
T: Send + 'static

when working with threads.

For example:

1
2
3
thread::spawn(move || {
    println!("{value}");
});

The spawned thread may continue running after the current scope ends.

Therefore Rust cannot allow the thread to hold a reference to a local variable that might disappear.

For example:

1
2
3
4
5
let s = String::from("hello");

thread::spawn(|| {
    println!("{s}");
});

A normal spawned thread cannot safely assume that s remains alive.

Using:

1
move

moves ownership into the thread:

1
2
3
thread::spawn(move || {
    println!("{s}");
});

Now the thread owns the String.

This is one reason why ownership is often preferable to borrowing for spawned threads.


44. Scoped Threads and Lifetimes

Scoped threads are different.

For example:

1
2
3
4
5
6
7
let s = String::from("hello");

thread::scope(|scope| {
    scope.spawn(|| {
        println!("{s}");
    });
});

The scope guarantees that the thread finishes before the borrowed data can go away.

Conceptually:

1
2
3
4
5
6
7
8
s:
|---------------------------|

scope:
     |-----------------|

thread:
       |-----------|

The thread cannot outlive the scope.

Therefore borrowing s is safe.

This is a great example of lifetimes being used to express relationships between concurrency and data.


45. Lifetime Bounds and Threads

This distinction is important:

1
2
3
4
5
6
7
thread::spawn
    ↓
thread may outlive current scope
    ↓
borrowed references generally cannot escape
    ↓
often requires 'static

while:

1
2
3
4
5
thread::scope
    ↓
threads must finish before scope ends
    ↓
borrowing local data is possible

Lifetimes are therefore deeply connected to Rust’s concurrency safety.


46. Lifetime on Trait Objects

You may encounter:

1
Box<dyn Trait>

and:

1
Box<dyn Trait + 'a>

The latter explicitly specifies a lifetime bound.

Example:

1
Box<dyn Display + 'a>

means the trait object may contain references valid for 'a.

You can also see:

1
&'a dyn Trait

which means:

A reference with lifetime 'a to a trait object.


47. dyn Trait + 'static

A common form is:

1
Box<dyn Trait + 'static>

This means the object does not contain non-'static borrowed references.

For example:

1
2
3
struct User {
    name: String,
}

can generally be stored in:

1
Box<dyn SomeTrait + 'static>

because String owns its data.

But a type containing a short-lived reference may not satisfy that bound.


48. Lifetime Bounds on Traits

You can define:

1
2
3
trait Trait<'a> {
    fn get(&self) -> &'a str;
}

Here the trait itself has a lifetime parameter.

A type can implement it:

1
2
3
4
5
6
7
8
9
struct Data<'a> {
    value: &'a str,
}

impl<'a> Trait<'a> for Data<'a> {
    fn get(&self) -> &'a str {
        self.value
    }
}

This means the trait’s behavior is explicitly associated with lifetime 'a.


49. Generic Types and Lifetimes Together

You can have both type parameters and lifetime parameters:

1
2
3
struct Container<'a, T> {
    value: &'a T,
}

Here:

1
2
'a → lifetime parameter
T  → type parameter

Example:

1
2
3
4
5
let value = 10;

let container = Container {
    value: &value,
};

50. Lifetime Bounds with Generic Types

Example:

1
2
3
4
5
fn process<'a, T>(value: &'a T)
where
    T: 'a,
{
}

This says:

1
T must be valid for at least 'a

This becomes especially relevant when T itself might contain references.


51. Higher-Ranked Trait Bounds

An advanced lifetime syntax is:

1
for<'a>

For example:

1
F: for<'a> Fn(&'a str) -> &'a str

This means:

F must work for every possible lifetime 'a.

This is called a higher-ranked trait bound (HRTB).

It is useful when a function or closure must accept references with arbitrary lifetimes.


52. Understanding for<'a>

Consider:

1
for<'a> Fn(&'a str) -> &'a str

Read it as:

For every lifetime 'a, this function can accept a &'a str and return a &'a str.

Conceptually:

1
2
3
4
'a = lifetime #1 → works
'a = lifetime #2 → works
'a = lifetime #3 → works
...

It is not one particular lifetime.

It means all possible lifetimes.


53. Anonymous Lifetimes

Rust also has anonymous lifetime syntax in certain contexts:

1
'_

For example:

1
2
fn foo(x: &'_ str) {
}

'_ means:

Let the compiler infer this lifetime.

You may also encounter:

1
impl Iterator<Item = &'_ str>

where the lifetime is intentionally left for inference.


54. '_ vs 'a

Compare:

1
&'a str

and:

1
&'_ str

'a is a named lifetime parameter.

For example:

1
2
3
fn foo<'a>(x: &'a str) -> &'a str {
    x
}

'_ is an anonymous lifetime:

1
2
fn foo(x: &'_ str) {
}

It tells Rust:

Infer this lifetime rather than giving it a name.


55. Lifetime Elision vs '_

These are related but not identical concepts.

This:

1
fn foo(x: &str) -> &str

uses lifetime elision rules.

This:

1
fn foo(x: &'_ str) -> &'_ str

explicitly uses anonymous lifetime syntax.

Usually, the first form is preferred when lifetime elision makes the relationship obvious.


56. Lifetime on References in Type Aliases

You can define a lifetime-aware type alias:

1
type StrRef<'a> = &'a str;

Then:

1
2
3
fn foo<'a>(value: StrRef<'a>) -> StrRef<'a> {
    value
}

This is useful when a reference type appears repeatedly.


57. Lifetime on Type Aliases with Generic Types

For example:

1
type Ref<'a, T> = &'a T;

Then:

1
2
3
fn get<'a, T>(value: Ref<'a, T>) -> Ref<'a, T> {
    value
}

58. Lifetime Parameters in Enums

Enums can contain references too:

1
2
3
4
enum Message<'a> {
    Text(&'a str),
    Number(i32),
}

Example:

1
2
3
let text = String::from("hello");

let message = Message::Text(&text);

The enum cannot outlive the referenced data.


59. Lifetime Parameters in Traits

Traits can have lifetime parameters:

1
2
3
trait Parser<'a> {
    fn parse(&self, input: &'a str);
}

Implementation:

1
2
3
4
5
6
7
struct MyParser;

impl<'a> Parser<'a> for MyParser {
    fn parse(&self, input: &'a str) {
        println!("{input}");
    }
}

60. Lifetime Parameters in Functions Returning References

Common pattern:

1
2
3
fn get<'a>(value: &'a String) -> &'a str {
    value.as_str()
}

This means:

1
2
3
4
5
input:
&'a String

output:
&'a str

The returned reference cannot outlive the input reference.


61. Lifetime on self

You can explicitly name the lifetime of self:

1
2
3
4
5
impl User {
    fn name<'a>(&'a self) -> &'a str {
        &self.name
    }
}

Usually this is unnecessary because Rust’s lifetime elision handles it:

1
2
3
4
5
impl User {
    fn name(&self) -> &str {
        &self.name
    }
}

The second form is idiomatic.


62. Lifetime on &mut self

Similarly:

1
2
3
4
5
impl User {
    fn name_mut<'a>(&'a mut self) -> &'a mut String {
        &mut self.name
    }
}

Usually written:

1
2
3
4
5
impl User {
    fn name_mut(&mut self) -> &mut String {
        &mut self.name
    }
}

Again, lifetime elision handles the relationship.


63. Lifetime Subtyping

Rust allows lifetime relationships where one lifetime outlives another.

For example:

1
'a: 'b

means:

1
'a lasts at least as long as 'b

This is sometimes described as lifetime subtyping.

Conceptually:

1
2
3
4
5
'a
|-------------------------|

'b
     |---------------|

The longer lifetime can be used where a shorter lifetime is required.


64. Reborrowing

Lifetimes also help explain reborrowing.

Consider:

1
2
3
4
5
6
7
let mut value = String::from("hello");

let r = &mut value;

r.push_str(" world");

r.push_str("!");

Rust can create shorter mutable borrows from r.

Conceptually:

1
2
3
4
5
6
7
8
original mutable borrow
|---------------------------|

first reborrow:
   |------|

second reborrow:
           |------|

This is one reason Rust can safely support complex mutable-reference code.


65. Lifetime and &mut

A mutable reference is exclusive for its active lifetime.

For example:

1
2
3
let mut x = 10;

let r = &mut x;

While that borrow is active, another conflicting borrow cannot be used.

This:

1
2
let r1 = &mut x;
let r2 = &mut x;

is invalid if both are simultaneously active.

The borrow checker tracks the relevant lifetimes.


66. Lifetimes and Interior Mutability

Types such as:

1
RefCell<T>

allow borrowing rules to be checked at runtime rather than compile time.

For example:

1
2
3
let value = RefCell::new(10);

let borrow = value.borrow();

The reference returned by the borrow still has a lifetime.

The difference is that RefCell manages the borrowing rules dynamically.

So:

1
2
3
4
5
6
7
normal reference
    ↓
borrow rules checked at compile time

RefCell
    ↓
borrow rules checked at runtime

Lifetimes remain part of the type system even though some borrow checking is moved to runtime.


67. Lifetimes and Rc

Rc<T> provides shared ownership.

For example:

1
2
3
4
5
6
use std::rc::Rc;

let value = Rc::new(String::from("hello"));

let a = Rc::clone(&value);
let b = Rc::clone(&value);

There is no lifetime parameter required for the Rc itself because Rc owns the data.

Conceptually:

1
2
3
4
Rc
 ├── owner #1
 ├── owner #2
 └── data

The data remains alive while at least one Rc exists.

This is different from:

1
&'a T

which is a borrowed reference and therefore needs lifetime reasoning.


68. Lifetimes and Box

Similarly:

1
Box<T>

owns its value.

For example:

1
let value = Box::new(String::from("hello"));

Box<T> does not need a lifetime parameter for the owned value.

Compare:

1
2
3
4
5
6
Box<T>
└── owns T

&'a T
└── borrows T
    └── lifetime required

This distinction is fundamental.


69. Owned vs Borrowed Data

A useful rule:

1
2
3
4
5
6
7
8
9
10
Owned:
String
Vec<T>
Box<T>
Rc<T>
Arc<T>

Borrowed:
&T
&mut T

Borrowed data requires lifetime reasoning.

Owned data generally does not require explicit lifetime parameters because ownership determines how long the data remains valid.


70. Lifetimes and Arc

Arc<T> is shared ownership across threads.

1
2
3
4
5
6
use std::sync::Arc;

let value = Arc::new(String::from("hello"));

let a = Arc::clone(&value);
let b = Arc::clone(&value);

Again, the Arc owns the data.

This is different from sharing:

1
&'a T

The Arc keeps the data alive through ownership.


71. Lifetimes and Send / Sync

Lifetimes also appear indirectly when working with concurrency.

For example, a spawned thread often requires:

1
Send + 'static

Send answers:

Can this value be transferred to another thread?

'static answers:

Does this value contain references that could become invalid before the thread is finished?

These are different properties.

For example:

1
String

can be Send and 'static.

But:

1
&String

may have a short lifetime and therefore cannot necessarily be sent to an independently running thread.


72. A Lifetime Does Not Mean Ownership

This is especially important.

Consider:

1
2
3
let s = String::from("hello");

let r = &s;

r has a lifetime.

But r does not own s.

The ownership relationship is:

1
2
3
4
5
s
└── owns String

r
└── borrows s

The lifetime describes:

1
how long r can safely be used

It does not transfer ownership.


73. Can We Talk About Ownership of &T?

Yes, but carefully.

A reference itself is a value.

Therefore:

1
2
let r1 = &s;
let r2 = r1;

moves or copies the reference value, depending on its type semantics.

But this does not mean:

1
r1 owns s

It means:

1
r1 contains a reference to s

References are themselves values, but they do not own the data they point to.

This distinction is important when reasoning about Send, Sync, ownership, and lifetimes.


74. Lifetime of a Reference vs Lifetime of Referenced Data

Suppose:

1
2
3
4
5
6
let s = String::from("hello");

{
    let r = &s;
    println!("{r}");
}

There are two different lifetimes:

1
2
3
4
5
s:
|-----------------------------|

r:
     |-----------|

The data’s lifetime is longer.

The reference’s lifetime is shorter.

This is normal.

A lifetime parameter does not mean:

The data itself lives exactly for 'a.

It means:

The reference is valid for 'a.


75. Lifetime of a Reference Can Be Shorter Than the Named Lifetime

When you write:

1
2
fn foo<'a>(x: &'a str) {
}

'a represents a lifetime chosen by the compiler for that call.

The actual borrow may be shorter than the entire lifetime of the underlying object.

This is why lifetime annotations should be understood as constraints and relationships, not timers.


76. Common Lifetime Syntax Cheat Sheet

Reference with lifetime

1
&'a T

Mutable reference with lifetime

1
&'a mut T

Function lifetime parameter

1
2
fn foo<'a>() {
}

Multiple lifetime parameters

1
2
fn foo<'a, 'b>() {
}

Input and output relationship

1
2
3
fn foo<'a>(x: &'a T) -> &'a T {
    x
}

Multiple inputs

1
2
3
4
5
fn foo<'a, 'b>(
    x: &'a T,
    y: &'b T,
) {
}

Struct with lifetime

1
2
3
struct Foo<'a> {
    value: &'a T,
}

Multiple lifetimes in a struct

1
2
3
4
struct Foo<'a, 'b> {
    first: &'a T,
    second: &'b T,
}

impl with lifetime

1
2
impl<'a> Foo<'a> {
}

Lifetime bound

1
T: 'a

Lifetime outlives relationship

1
'a: 'b

where lifetime bound

1
2
where
    T: 'a,

Trait with lifetime

1
2
trait Foo<'a> {
}

Trait implementation with lifetime

1
2
impl<'a> Foo<'a> for Bar<'a> {
}

Trait object with lifetime

1
dyn Trait + 'a

Reference to trait object

1
&'a dyn Trait

Boxed trait object

1
Box<dyn Trait + 'a>

Static lifetime

1
'static

Anonymous lifetime

1
'_

Higher-ranked trait bound

1
for<'a>

Example:

1
F: for<'a> Fn(&'a str) -> &'a str

77. A Practical Decision Process

When you see a lifetime error, ask these questions in order.

1. Who owns the data?

1
2
3
4
5
String?
Vec?
Box?
Rc?
Arc?

If something owns the data, determine whether ownership can be returned or moved instead of borrowing.

2. Who is borrowing?

Look for:

1
2
&T
&mut T

3. How long does the owner live?

Find the scope where the owned value is valid.

4. How long does the reference need to live?

Find where the reference is used.

5. Does the reference outlive its owner?

If yes, the program is invalid.

6. Does the function return a reference?

If yes, ask:

Which input does the returned reference come from?

7. Can lifetime elision express the relationship?

If yes, you probably don’t need explicit lifetime syntax.


78. The Most Common Lifetime Pattern

You will encounter this pattern constantly:

1
2
3
fn get<'a>(value: &'a T) -> &'a U {
    // ...
}

The key idea is:

1
2
3
4
5
6
7
8
9
input borrow
    │
    │ 'a
    ▼
function
    │
    │ 'a
    ▼
output borrow

The output cannot outlive the input reference.


79. Another Common Pattern: Struct Holding a Reference

1
2
3
struct Parser<'a> {
    input: &'a str,
}

Think:

1
2
3
4
5
Parser<'a>
    │
    └── borrows input
            │
            └── input must remain alive

This is common in parsers and zero-copy data structures.


80. Zero-Copy Programming

Lifetimes are especially important in zero-copy programming.

Instead of creating a new String:

1
2
3
fn get_name(input: &str) -> String {
    input[..5].to_string()
}

you can return a slice:

1
2
3
fn get_name<'a>(input: &'a str) -> &'a str {
    &input[..5]
}

Now the result borrows the original data.

Conceptually:

1
2
3
4
5
6
7
8
9
input:
"Amir Nikpour"
 │
 └────────────── owned data

result:
&input[..5]
 │
 └── points into existing data

No new string allocation is necessary.

This is one of the major practical benefits of lifetimes.


81. Lifetimes Enable Safe Zero-Copy APIs

Consider:

1
2
3
struct User<'a> {
    name: &'a str,
}

The User does not own the name.

It borrows it:

1
2
3
4
5
6
7
8
9
input buffer
┌──────────────────────────┐
│ Amir Nikpour             │
└──────────────────────────┘
          ▲
          │
          │ &'a str
          │
       User<'a>

Rust ensures the User cannot outlive the input buffer.

This allows efficient zero-copy designs while maintaining memory safety.


82. Lifetime Errors Are Often Design Errors

When Rust gives you a lifetime error, don’t immediately try to “fight the borrow checker.”

First ask:

What ownership relationship am I trying to express?

For example, if you write:

1
2
3
4
fn create() -> &str {
    let s = String::from("hello");
    &s
}

adding random lifetime annotations cannot fix the problem.

The actual problem is:

1
2
3
4
5
6
7
function owns data
        ↓
function ends
        ↓
data destroyed
        ↓
reference would become invalid

The correct design is probably:

1
2
3
fn create() -> String {
    String::from("hello")
}

83. When Should You Add an Explicit Lifetime?

Do not add lifetime annotations everywhere.

Usually add them when:

  • a function returns a reference
  • multiple input references are involved
  • a struct stores references
  • a trait has lifetime-dependent behavior
  • you need to express an outlives relationship
  • the compiler cannot infer the relationship
  • you are implementing advanced generic abstractions

For simple borrowing:

1
2
3
fn print(value: &str) {
    println!("{value}");
}

is better than:

1
2
3
fn print<'a>(value: &'a str) {
    println!("{value}");
}

The explicit lifetime adds no useful information here.


84. When Should You Avoid Lifetimes?

Sometimes the easiest solution is to use ownership.

Instead of:

1
2
3
struct User<'a> {
    name: &'a str,
}

you can use:

1
2
3
struct User {
    name: String,
}

Now:

1
2
User
 └── owns name

rather than:

1
2
User<'a>
 └── borrows name

Owned data often makes APIs simpler.

Borrowed data can be useful when:

  • performance matters
  • allocations should be avoided
  • data is naturally borrowed
  • zero-copy parsing is desired
  • the lifetime relationship is clear

85. Lifetimes Are Part of Rust’s Memory Safety Model

Rust’s memory safety comes from several concepts working together:

1
2
3
4
5
6
7
8
9
Ownership
    +
Borrowing
    +
Lifetimes
    +
Type system
    +
Borrow checker

Ownership answers:

Who is responsible for the data?

Borrowing answers:

Who can temporarily access the data?

Lifetimes answer:

How long is that access valid?

The borrow checker uses these rules to prevent problems such as:

  • use-after-free
  • dangling references
  • invalid mutable aliases
  • references to destroyed stack variables

86. Lifetimes and Dangling References

Languages with manual memory management can accidentally create:

1
pointer → freed memory

Rust prevents this.

For example:

1
2
3
4
fn bad() -> &String {
    let value = String::from("hello");
    &value
}

The compiler detects that:

1
2
3
4
5
value dies
    ↓
returned reference would remain
    ↓
dangling reference

Therefore the program is rejected before it runs.


87. A Good Mental Model

Don’t think:

'a means this object lives for some fixed amount of time.

Think:

'a is a name for a region of validity of a reference, and annotations tell Rust how different references are related.

For example:

1
2
3
4
fn longest<'a>(
    x: &'a str,
    y: &'a str,
) -> &'a str

Think:

1
2
3
4
5
6
7
8
9
10
          'a
           │
     ┌─────┴─────┐
     │           │
   x: &'a      y: &'a
     │           │
     └─────┬─────┘
           │
        returned
         &'a str

The annotation tells the compiler:

The returned reference is tied to the lifetime of the input references.


88. Final Mental Model

The easiest way to remember lifetimes is:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Ownership
    │
    └── Who owns the data?

Borrowing
    │
    └── Who temporarily accesses it?

Lifetime
    │
    └── How long is that reference valid?

Borrow checker
    │
    └── Are all these relationships safe?

And when reading Rust code:

1
'a

think:

“A named lifetime relationship.”

1
&'a T

think:

“A reference to T valid for 'a.”

1
fn foo<'a>(x: &'a T) -> &'a T

think:

“The returned reference is tied to the input reference.”

1
'a: 'b

think:

”'a outlives 'b.”

1
T: 'a

think:

“T is valid for at least 'a.”

1
'static

think:

“Valid for the entire program, or, for a type bound, contains no shorter-lived borrowed references.”

1
'_

think:

“Let the compiler infer this lifetime.”

1
for<'a>

think:

“For every possible lifetime 'a.”

The central idea is:

Lifetimes do not control how long data lives. Ownership and scope determine that. Lifetimes describe and constrain how long references can safely be used in relation to the data they borrow.

Once you see lifetimes as relationships between references rather than timers, most lifetime syntax becomes much easier to understand.

This post is licensed under CC BY 4.0 by the author.