Rust's ownership and borrowing system is one of its superpowers—it eliminates entire classes of bugs like dangling pointers and data races at compile time. But for many newcomers, lifetimes feel like the most confusing part. You might have read that "lifetimes mark how long a reference lives," but that explanation often falls short.

In this post, we'll go deeper. Lifetimes aren't just about duration—they're about relating and constraining multiple references together to prove safety to the borrow checker. We'll cover the intuition, the rules, special lifetimes, elision, and real-world examples. By the end, lifetimes should feel less like magic and more like logical constraints.

Why Lifetimes Exist

Rust guarantees memory safety without a garbage collector by tracking ownership and borrows. References (&T or &mut T) must always point to valid data. Valid data is a simple definition: it must be alive, meaning it most not be droped. For a primitive data like i32, it just needs to be in scope. but for a value allocated on heap via Box, it must not be freed.

In general, languages provide memory leakage preventation by two means:

  1. Reference counting: Add a reference count to data, and whenever it is borrowed, increment it. when a reference goes out of scope, decrement it. if after decreasing refcount reaches zero, memory is not needed and can be freed. For example in C++, std::shared_ptr is a reference counting primitive, or in python, all objects have a refcount in their C implementation.
  2. Garbage collection: a dedicated thread contineuoslly polls for unused data and free them. Such languages like Go, and Java use this mechanism.

Rust does some kind of reference counting and borrow checking at Compile time, so no refcount is added to data.

The compiler needs to know:

  • How long each reference is valid, it means it points to a valid data.
  • That no reference outlives the data it points to.

In simple cases, the compiler infers this automatically. In complex cases (functions returning references, structs holding references), you may help it with lifetime annotations.

Think of lifetimes as labels that group references and enforce dependency rules: "This reference must not outlive that one."

The Core Intuition: Grouping + Dependency

A lifetime like 'a does two things:

  1. Groups references together: By specifying a lifetime, you instruct the compiler: "Consider these references as part of the same lifetime bucket". It clears the situation for compiler that these references are somehow related and their life depends on each other.
  2. Establishes dependency: Any reference with lifetime 'a is dependent on something that lives at least as long as 'a. In other words, it must not outlive its dependencies. References that takes their underlaying value from other references, depend on them. For example a return reference from a function depends on the arguments, as it's underlaying value is from them.

Let's consider this classic example:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

Here:

  • Both inputs and the output are grouped under 'a, meaning these must be considerd toghether in borrow checking.
  • The returned reference depends on 'a.
  • 'a is effectively the minimum (shortest) of the two input lifetimes. In other words, as the returned reference may depend on each of inputs, and we don't know wich, it must not live longer than any of the arguments that lives shorter.
  • This ensures the returned reference can't dangle—if the shorter input drops, the output is invalid anyway.

Without the same 'a, the compiler couldn't prove safety because the return might borrow from the shorter-lived input.

Now, lets clarify this with another example that doesn't have same lifetime qualifier for all arguments. Suppose we want to remove a prefix from an string. It is clear that the prefix itself is not needed to live after returning the result, so it may not need to be in same lifetime checking:

fn remove_prefix<'a,'b>(prefix: &'b str, subject: &'a str) -> &'a str {
    // existance of prefix is done here
    // ...
    &subject[len(prefix)..]
}

We now that as the function returns an slice that it's content is borrowed from subject, it cannot outlive it. But after the function call, it doesn't depend on existance of prefix. So we mark subject and returning reference as 'a, to indicate the must be considered toghether, and mark prefix as 'b, to make it irrelevant to other. Now, compiler will not check for lifetime dependency of returning reference on prefix, Just subject!

Special Lifetime Specifiers

'static – The Forever Lifetime

'static is a concrete lifetime meaning data lives for the entire program duration. This ensures the compiler that it doesn't drop anytime, and it is safe to relay on it's existance. It simplifies the borrow checking as the references with 'static lifetime, outlives any other ones.

Amoung common data, thses ones are intersting:

  • String literals are 'static: "hello" is &'static str. As they are hardcoded into program's data section.
  • Globals with static or const often are too.
  • Required for things like thread spawning (threads might outlive the current scope).
let greeting: &'static str = "Hello, world!";

thread::spawn(|| {
    println!("{}", greeting); // OK because 'static
});

Every 'static reference satisfies any other lifetime (it outlives everything).

'_ – The Anonymous Lifetime

'_ means "infer this lifetime for me." It's perfect for avoiding noisy annotations.

Common in impl blocks:

struct Parser<'a> { input: &'a str }

impl<'a> Parser<'a> {
    fn parse(&self) -> &'a str { self.input }
}

// Idiomatic version:
impl Parser<'_> {
    fn parse(&self) -> &str { self.input } // Elision helps here too
}

Lifetimes in Structs

If a struct holds references, It become dependent to their underlaying data. it means the objects of this struct, must not outlive the values of reference fields. So it needs lifetime parameters. It is not new concept, It borrows some data, and should live shorter than it's borrow fields.

// Single lifetime (elidable in Rust 2018+)
struct Excerpt<'a> {
    part: &'a str,
}

// Equivalent (elided)
struct Excerpt {
    part: &str, // Compiler adds the lifetime automatically
}

// Multiple independent lifetimes
struct DoubleRef<'a, 'b> {
    first: &'a str,
    second: &'b str,
}

Elision only works when all reference fields share one lifetime.

I this situation, we know that objects depend on all of the references they hold. So it must not outlive eny of them. Compiler consider both (or all) of the lifetimes, and checks for shortest one. Using multiple lifetimes in structs, doesn't mean that it is irrelevant to any of them, like what we saw in function example, it is an AND operator on lifetimes.

Lifetimes in Methods

Methods on lifetime-containing structs follow similar rules.

impl<'a> Excerpt<'a> {
    fn part(&self) -> &'a str { self.part }

    fn announce(&self, msg: &str) -> &str {
        println!("{}", msg);
        self.part // Returns reference tied to self
    }
}

With '_ for cleanliness:

impl Excerpt<'_> {
    fn part(&self) -> &str { self.part }
}

If a method takes &self and other references, and returns a reference, you often need to specify which lifetime it ties to.

Lifetime Elision Rules (The Magic That Hides Annotations)

Rust infers lifetimes in many cases using three rules:

  1. Each input reference gets its own lifetime.

    fn foo(x: &str, y: &str) // becomes fn foo<'a, 'b>(x: &'a str, y: &'b str)
    
  2. If there's exactly one input lifetime, assign it to all outputs.

    fn first_word(s: &str) -> &str // becomes &'a str -> &'a str
    
  3. If multiple inputs, but one is &self or &mut self, use self's lifetime for outputs.

    // Common in methods
    fn get(&self) -> &str // return uses self's lifetime
    

These apply to functions, methods, and (limited) struct fields.

Dependency Rules

As we mentioned, life time for a reference is related and limited by lifespan of other references or objects. As a rule of thumb, newly created references, are dependent to preveously created ones. This rules are summorized in following table.

ConstructIndependent referencesDependent references
Function definitionAll reference argumentsAny returning reference
Struct definitionReferences you get to set fieldsReference fileds, generally the whole object
Method definition&self, &mut selfAny returning reference

Common Pitfalls and Tips

  • You can't return a reference to a local variable (dangling).
  • Use String or Cow<str> when you need owned data instead of borrowing.
  • Read borrow checker errors—they're surprisingly helpful!
  • Practice with the Rust Book's lifetime chapter—it's excellent.

Conclusion

Lifetimes aren't about measuring time in seconds—they're about proving relationships between references so Rust can guarantee no dangling pointers.

Once you internalize:

  • Grouping with the same lifetime name,
  • Dependency (must not outlive),
  • Elision handling the simple cases,

...the syntax stops feeling arbitrary and starts feeling like a powerful safety net.

If you're learning Rust, embrace the borrow checker errors—they're teaching you to write better code. Keep practicing, and lifetimes will click!

Happy borrowing! 🦀