Skip to main content
Rust · lesson 10 of 30
Rust intermediate Lesson 10 of 30

Lifetimes in Rust

Understand Rust lifetimes: annotation syntax, elision rules, lifetimes in structs, 'static, and the real compiler errors lifetimes prevent.

By SkillByExample · Updated

What Are Lifetimes?

Every reference in Rust has a lifetime — the stretch of code for which the reference is valid. Most of the time the compiler infers lifetimes automatically. You only need to write explicit lifetime annotations when the compiler cannot figure out the relationship between the lifetimes of multiple references on its own.

Lifetimes exist to prevent dangling references. Here r outlives the value it points to:

fn main() {
    let r;
    {
        let x = 5;
        r = &x;
    } // x is dropped here
    println!("{}", r); // r would point to freed memory
}
$ rustc lifetimes_what_are_lifetimes.rs
error[E0597]: `x` does not live long enough
 --> lifetimes_what_are_lifetimes.rs:5:13
  |
4 |         let x = 5;
  |             - binding `x` declared here
5 |         r = &x;
  |             ^^ borrowed value does not live long enough
6 |     } // x is dropped here
  |     - `x` dropped here while still borrowed
7 |     println!("{}", r); // r would point to freed memory
  |                    - borrow later used here

The compiler rejects the program: x is dropped at the inner }, but r — which borrows it — is still used afterwards. Delete the println! and the program compiles, because a borrow only has to be valid for as long as it is actually used, not for the whole scope of the variable holding it.

Lifetime Annotation Syntax

Lifetime parameters are written with a leading apostrophe: 'a, 'b, 'input, etc. They appear after & in reference types and in angle brackets on function/struct signatures. A function that returns one of two borrowed inputs needs one:

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

fn main() {
    println!("{}", longest("long string", "xyz"));
}
$ rustc lifetimes_lifetime_annotation_syntax.rs
error[E0106]: missing lifetime specifier
 --> lifetimes_lifetime_annotation_syntax.rs:1:33
  |
1 | fn longest(x: &str, y: &str) -> &str {
  |               ----     ----     ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
  |
1 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
  |           ++++     ++          ++          ++

The compiler cannot tell whether the returned reference borrows from x or from y, so it cannot check how long the result may be used. Annotating both inputs and the output with the same 'a answers that question:

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

fn main() {
    let s1 = String::from("long string");
    let result;
    {
        let s2 = String::from("xyz");
        result = longest(s1.as_str(), s2.as_str());
        println!("longest: {}", result); // OK — result used inside s2's scope
    }
}
$ rustc lifetimes_lifetime_annotation_syntax_2.rs && ./lifetimes_lifetime_annotation_syntax_2
longest: long string

The annotation 'a says: “the returned reference is valid only as long as both x and y are” — in practice, the shorter of the two. The annotation doesn’t change how long anything lives; it lets the compiler reject a call site where the result would be used after either input is gone.

Lifetime Elision

Rust has three elision rules that let the compiler infer lifetimes in common cases:

  1. Each reference parameter gets its own lifetime.
  2. If there is exactly one input lifetime, it is assigned to all output lifetimes.
  3. If one of the inputs is &self or &mut self, its lifetime is assigned to all output lifetimes.

Both functions below compile with no annotations at all — rule 2 covers first_word, rule 3 covers get_name:

struct User {
    name: String,
}

impl User {
    // rule 3: inferred as fn get_name<'s>(&'s self) -> &'s str
    fn get_name(&self) -> &str {
        &self.name
    }
}

// rule 2: inferred as fn first_word<'a>(s: &'a str) -> &'a str
fn first_word(s: &str) -> &str {
    let i = s.find(' ').unwrap_or(s.len());
    &s[..i]
}

fn main() {
    let user = User { name: String::from("Ada Lovelace") };
    println!("{}", user.get_name());
    println!("{}", first_word(user.get_name()));
}
$ rustc lifetimes_lifetime_elision.rs && ./lifetimes_lifetime_elision
Ada Lovelace
Ada

longest in the previous section fails precisely because none of these rules apply: it has two reference inputs and no self, so rule 2 and rule 3 both stay silent and the output lifetime is left undetermined.

Lifetimes in Structs

If a struct holds a reference, it must have a lifetime annotation to guarantee the struct doesn’t outlive the data it references:

struct Important<'a> {
    text: &'a str,
}

impl<'a> Important<'a> {
    fn announce(&self, topic: &str) -> &str {
        println!("Attention regarding {}: {}", topic, self.text);
        self.text
    }
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let i = novel.find('.').unwrap_or(novel.len());
    let important = Important { text: &novel[..i] };
    let returned = important.announce("openings");
    println!("returned: {}", returned);
}
$ rustc lifetimes_lifetimes_in_structs.rs && ./lifetimes_lifetimes_in_structs
Attention regarding openings: Call me Ishmael
returned: Call me Ishmael

The lifetime 'a ties Important to novel: the struct cannot be used after novel is dropped. announce returns &str with no annotation — elision rule 3 gives it the lifetime of &self.

Multiple Lifetime Parameters

When references have genuinely independent lifetimes, give them separate parameters. Here the result only ever comes from the strings, never from the slice that holds them:

fn first_or_default<'a, 'b>(slice: &'a [&'b str], default: &'b str) -> &'b str {
    slice.first().copied().unwrap_or(default)
}

fn main() {
    let picked;
    {
        let names = vec!["alice", "bob"]; // the Vec is dropped at the end of this block
        picked = first_or_default(&names, "nobody");
    }
    println!("{}", picked); // still fine: picked borrows a 'static literal, not the Vec
}
$ rustc lifetimes_multiple_lifetime_parameters.rs && ./lifetimes_multiple_lifetime_parameters
alice

Because the return type uses 'b, not 'a, the result is allowed to outlive the Vec it was read from. Collapse both parameters into one 'a and this program no longer compiles — the result would be tied to names, which is gone by the time println! runs. Keep annotations minimal, but introduce a new parameter when the relationships genuinely differ.

The 'static Lifetime

'static means the reference can be valid for the entire program. String literals are baked into the binary, so they are always 'static:

fn needs_static(s: &'static str) {
    println!("{}", s);
}

fn main() {
    let literal: &'static str = "I have a static lifetime";
    needs_static(literal);

    let owned = String::from("I live on the heap");
    let leaked: &'static str = Box::leak(owned.into_boxed_str());
    needs_static(leaked);
}
$ rustc lifetimes_the_static_lifetime.rs && ./lifetimes_the_static_lifetime
I have a static lifetime
I live on the heap

Box::leak turns an owned heap value into a 'static reference by promising never to free it — the memory is reclaimed only when the process exits, so use it sparingly. Passing &owned directly to needs_static would not compile: a borrow of a local String cannot be 'static.

Common sources of 'static data:

  • String literals
  • Box::leak — leaks heap memory to produce a 'static reference
  • static items and lazily-initialized statics (std::sync::OnceLock, std::sync::LazyLock)

Do not use 'static bounds just to avoid thinking about lifetimes — it is often too restrictive.

Lifetime Bounds on Generics

Lifetime parameters and generic type parameters can appear in the same signature:

use std::fmt::Display;

fn longest_with_announcement<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a str
where
    T: Display,
{
    println!("Announcement: {}", ann);
    if x.len() > y.len() { x } else { y }
}

fn main() {
    let winner = longest_with_announcement("hello", "hi", 42);
    println!("winner: {}", winner);
}
$ rustc lifetimes_lifetime_bounds_on_generics.rs && ./lifetimes_lifetime_bounds_on_generics
Announcement: 42
winner: hello

T can be any Display type — here an integer — while 'a still governs only the two string references.

Common Lifetime Errors and Fixes

Returning a reference to local data. No lifetime annotation can fix this — the data is gone when the function returns:

fn bad() -> &str {
    let s = String::from("hello");
    &s
}

fn main() {
    println!("{}", bad());
}
$ rustc lifetimes_common_lifetime_errors_and_fixes.rs
error[E0106]: missing lifetime specifier
 --> lifetimes_common_lifetime_errors_and_fixes.rs:1:13
  |
1 | fn bad() -> &str {
  |             ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`
  |
1 | fn bad() -> &'static str {
  |              +++++++
help: instead, you are more likely to want to return an owned value
  |
1 - fn bad() -> &str {
1 + fn bad() -> String {
  |

The compiler reports the missing lifetime, but its help text points at the real fix: return an owned String instead of a reference.

fn good() -> String {
    String::from("hello")
}

fn main() {
    println!("{}", good());
}
$ rustc lifetimes_common_lifetime_errors_and_fixes_2.rs && ./lifetimes_common_lifetime_errors_and_fixes_2
hello

A struct holding a reference without a lifetime parameter:

struct Excerpt {
    text: &str,
}

fn main() {
    let e = Excerpt { text: "hi" };
    println!("{}", e.text);
}
$ rustc lifetimes_common_lifetime_errors_and_fixes_3.rs
error[E0106]: missing lifetime specifier
 --> lifetimes_common_lifetime_errors_and_fixes_3.rs:2:11
  |
2 |     text: &str,
  |           ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
1 ~ struct Excerpt<'a> {
2 ~     text: &'a str,
  |

The fix is exactly what the compiler suggests: struct Excerpt<'a> { text: &'a str }, as in the Important example above.

Lifetimes are Not Runtime Costs

Lifetime annotations are purely compile-time constructs. They generate zero machine code. They are the compiler’s way of tracking how long references are valid, not runtime metadata.

Summary

ConceptMeaning
'aA named lifetime parameter
fn f<'a>(x: &'a T) -> &'a TOutput reference lives as long as input
struct S<'a> { r: &'a T }Struct cannot outlive the reference it holds
'staticReference valid for the entire program
ElisionCompiler infers lifetimes for common patterns
No annotation neededWhen elision rules fully determine all lifetimes

Practice

  1. In the working longest example, move println!("longest: {}", result); out of the inner block so it runs after s2 is dropped. What does the compiler say?

    Solution
    error[E0597]: `s2` does not live long enough
    ...
    11 |     }
       |     - `s2` dropped here while still borrowed
    12 |     println!("longest: {}", result);
       |                             ------ borrow later used here

    'a ties result to the shorter-lived input. It doesn’t matter that s1 happens to be the longer string at runtime — the compiler checks lifetimes from the signature, not from which branch the if takes.

  2. Keep the println! outside the block, but change s2 to a string literal: let s2 = "xyz"; and pass s2 directly. Does it compile now?

    Solution

    Yes — it prints longest: long string. A literal is &'static str, so the shorter of the two input lifetimes is now s1’s, which lasts until the end of main.

  3. Collapse first_or_default’s two lifetime parameters into one: fn first_or_default<'a>(slice: &'a [&'a str], default: &'a str) -> &'a str. Keep main unchanged. What happens?

    Solution
    error[E0597]: `names` does not live long enough

    With a single 'a, the result is tied to the borrow of names itself, which ends when the inner block closes. Two parameters let the signature say the truth: the result comes from the strings ('b), not from the Vec holding them ('a).

  4. Call needs_static(&owned) with let owned = String::from("I live on the heap"); instead of leaking it. Read the error and explain which phrase names the problem.

    Solution

    error[E0597]: \owned` does not live long enough, with the key label argument requires that `owned` is borrowed for `‘static`. A local Stringis dropped at the end ofmain, so a borrow of it can never satisfy ’static`.

  5. Write fn last_word(s: &str) -> &str without any lifetime annotations, returning everything after the last space. Which elision rule makes it compile?

    Solution
    fn last_word(s: &str) -> &str {
        let i = s.rfind(' ').map(|i| i + 1).unwrap_or(0);
        &s[i..]
    }

    last_word("Ada Lovelace") returns Lovelace and last_word("Ada") returns Ada. Rule 2 applies: there is exactly one input lifetime, so the output gets it.

Frequently Asked Questions

What are lifetimes in Rust?
Lifetimes are annotations that tell the compiler how long references are valid. They don't change how long data lives — they just help the compiler verify that references don't outlive the data they point to.
What is lifetime elision?
Lifetime elision is a set of rules that let the compiler infer lifetimes in common patterns, so you don't have to write them explicitly most of the time.
What is the 'static lifetime?
'static means the reference is valid for the entire program lifetime. String literals have type &'static str because they are baked into the binary.