Syntax Cache
BlogMethodFeaturesHow It WorksBuild a Game
  1. Home
  2. Rust
  3. Rust References & Lifetimes Practice: Lifetime Annotations & Elision
Rust38 exercises

Rust References & Lifetimes Practice: Lifetime Annotations & Elision

Master Rust lifetime annotations and reference semantics. Learn when to annotate, elision rules, and how to fix lifetime errors.

Common ErrorsQuick ReferencePractice
Warm-up1 / 2

Can you write this from memory?

Given `let s = String::from("hello");`, create an immutable reference `r` to `s`.

On this page
  1. 1Compiler Error E0515: Cannot Return Reference to Local Variable
  2. 2Compiler Error E0106: Missing Lifetime Specifier
  3. 3Compiler Error E0597: Borrowed Value Does Not Live Long Enough
  4. 4Compiler Error E0716: Temporary Value Dropped While Borrowed
  5. 5Lifetime Elision Rules
  6. 6Structs That Store References
  7. 7'static: Two Different Meanings
  8. 8Lifetime Bounds
  9. 9Further Reading
Compiler Error E0515: Cannot Return Reference to Local VariableCompiler Error E0106: Missing Lifetime SpecifierCompiler Error E0597: Borrowed Value Does Not Live Long EnoughCompiler Error E0716: Temporary Value Dropped While BorrowedLifetime Elision RulesStructs That Store References'static: Two Different MeaningsLifetime BoundsFurther Reading

Lifetimes are Rust's way of ensuring references are always valid. The compiler tracks how long each reference lives and rejects code where a reference might outlive its data.

Most of the time, Rust figures out lifetimes automatically through "elision rules." But sometimes you need explicit annotations, and that's where the syntax gets tricky. Is it &'a str or 'a &str? What does 'static actually mean?

This page focuses on lifetime syntax until annotations become natural. From elision rules to explicit annotations to the common errors that send you searching.

Related Rust Topics
Rust Ownership & Borrowing: Move Semantics & Borrow RulesRust Traits & Generics: Trait Bounds, impl Trait, Turbofish

The most common lifetime mistake. A function creates a value and tries to return a reference to it:

// WRONG: returning a reference to a local variable
// fn make_greeting() -> &str {
//     let s = String::from("hello");
//     &s  // error[E0515]: cannot return reference to local variable
// }

// RIGHT: return an owned String instead
fn make_greeting() -> String {
    String::from("hello")
}

The local String is dropped at the end of the function. A reference to it would point at freed memory. If the function creates the data, it must return the owned value. References only work for data that outlives the function call. For a refresher on when values are dropped, see the ownership and borrowing guide.

Ready to practice?

Start practicing Rust References & Lifetimes: Lifetime Annotations & Elision with spaced repetition

When a function has multiple reference parameters and returns a reference, you must annotate lifetimes to tell the compiler which input the return borrows from. The annotation creates a relationship, not a new lifetime.

The compiler cannot infer which input reference the return borrows from:

// WRONG: two inputs, one output — which does the return borrow from?
// fn longest(x: &str, y: &str) -> &str {
//     if x.len() > y.len() { x } else { y }
//     // error[E0106]: missing lifetime specifier
// }

// RIGHT: tell the compiler both inputs and the output share a lifetime
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

The annotation 'a creates a relationship: at each call site, the returned reference is only valid for the shorter of the two input lifetimes, because both must be valid:

let result;
let string1 = String::from("long string");
{
    let string2 = String::from("xyz");
    result = longest(&string1, &string2);
    println!("{result}");  // OK: both string1 and string2 are alive
}
// println!("{result}");  // ERROR: string2 is dropped, so 'a has ended

A reference outlives the data it points to:

// WRONG: r outlives x
// let r;
// {
//     let x = 5;
//     r = &x;  // error[E0597]: x does not live long enough
// }
// println!("{r}");

// RIGHT: ensure the data lives at least as long as the reference
let x = 5;
let r = &x;
println!("{r}");

The fix is usually to move the owned value to a wider scope, or restructure so the reference doesn't escape.

A temporary (created mid-expression) is dropped at the end of the statement, but a reference to it escapes:

// WRONG: the String is a temporary, dropped at the semicolon
// let s: &str = &String::from("hello");  // error[E0716]

// RIGHT: bind the temporary to a variable first
let owned = String::from("hello");
let s: &str = &owned;  // owned lives long enough

This often appears in method chains that return owned values. Break the chain: bind the intermediate to a let.

Three rules: (1) each reference parameter gets its own lifetime, (2) one input lifetime → assigned to all outputs, (3) &self/&mut self lifetime → assigned to all outputs. Only annotate when these rules aren't enough.

Rust applies three rules to infer lifetimes automatically. You only need explicit annotations when these rules don't resolve everything:

  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 there is a &self or &mut self, its lifetime is assigned to all output lifetimes.
// Rule 2: one input → one output (no annotation needed)
fn first_word(s: &str) -> &str {
    s.split_whitespace().next().unwrap_or("")
}

// Rule 3: &self method (no annotation needed)
impl<'a> Excerpt<'a> {
    fn part(&self) -> &str {
        self.part
    }
}

// Elision FAILS: two inputs, one output — which does it borrow from?
// fn longest(x: &str, y: &str) -> &str  // error[E0106]
// Must annotate: fn longest<'a>(x: &'a str, y: &'a str) -> &'a str

Elision is only allowed when inference is unambiguous. When the compiler asks for annotations, it means the rules weren't enough. For a compact reference of these rules and common annotation patterns, see the lifetimes cheat sheet.

If a struct holds a reference, it needs a lifetime parameter. This tells the compiler: "this struct cannot outlive the data it borrows."

struct Excerpt<'a> {
    part: &'a str,
}

let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = &novel[..16];
let excerpt = Excerpt { part: first_sentence };
// excerpt cannot outlive novel

Every reference field adds a constraint. If the struct borrows from two independent sources, use two lifetime parameters:

struct Pair<'a, 'b> {
    first: &'a str,
    second: &'b str,
}

&'static T is a reference valid forever (like string literals). T: 'static means T contains no short-lived borrows—an owned String satisfies 'static. These are different concepts.

'static appears in two contexts with different meanings:

SyntaxMeaningExample
&'static TA reference valid for the entire programlet s: &'static str = "hello";
T: 'staticT contains no non-'static borrows (T is "self-contained")fn spawn<T: Send + 'static>(f: T)
// &'static str — string literals live forever
let s: &'static str = "hello";

// T: 'static — the type owns all its data (no borrowed fields)
// String satisfies 'static because it owns its data
// &'a str does NOT satisfy 'static (unless 'a is 'static)
fn send_to_thread<T: Send + 'static>(val: T) {
    std::thread::spawn(move || {
        println!("{val:?}");
    });
}

T: 'static does not mean "lives forever" — it means "doesn't borrow anything short-lived." An owned String is 'static, but a &str with a limited lifetime is not.

Lifetime bounds express relationships between lifetimes and between lifetimes and types:

// 'a: 'b means 'a outlives 'b (data behind 'a lives at least as long as 'b)
fn first<'a, 'b>(x: &'a str, _y: &'b str) -> &'a str
where
    'a: 'b,
{
    x
}

// T: 'a means T doesn't contain references shorter than 'a
fn store_ref<'a, T: 'a>(val: &'a T) {
    // val is valid for 'a, and T contains nothing shorter
}

The most common lifetime bound is T: 'static, seen in thread::spawn, trait objects (Box<dyn Trait + 'static>), and async runtimes. The traits and generics guide covers how trait bounds and lifetime bounds interact in practice.

  • The Rust Book: Lifetimes — annotations, elision, structs with references
  • Rust By Example: Lifetimes — practical lifetime examples
  • Rust Reference: Lifetime Elision — formal elision rules
  • Common Rust Lifetime Misconceptions — deep dive on 'static and other confusions

When to Use Rust References & Lifetimes: Lifetime Annotations & Elision

  • Add lifetime annotations when the compiler can't infer relationships.
  • Use `'static` for data that lives for the entire program (string literals, etc.).
  • Use explicit lifetimes when returning references from functions with multiple reference parameters.
  • Annotate struct fields that contain references.
  • Trust elision rules for simple cases. Don't over-annotate.

Check Your Understanding: Rust References & Lifetimes: Lifetime Annotations & Elision

Prompt

What are lifetime annotations and why are they needed?

What a strong answer looks like

Lifetime annotations describe how long references are valid relative to each other. They don't change how long data lives. They help the compiler verify that references won't outlive their data. They're needed when the compiler can't infer the relationships automatically.

What You'll Practice: Rust References & Lifetimes: Lifetime Annotations & Elision

Understand lifetime annotation syntax (&'a T)Know when annotations are required vs elidedApply the three lifetime elision rulesDistinguish &'static T from T: 'staticAnnotate struct fields with lifetimesHandle functions with multiple reference parametersUse lifetime bounds ('a: 'b, T: 'a)Fix E0106, E0515, E0597, E0716

Common Rust References & Lifetimes: Lifetime Annotations & Elision Pitfalls

  • Syntax error on `'a &str` -- the lifetime goes between `&` and the type. Write `&'a str`, not `'a &str`.
  • Adding `'a` everywhere to silence the compiler -- lifetimes describe relationships, not ownership. Over-annotating hides the real issue. Let elision rules work and only annotate when the compiler asks.
  • `error[E0515]` when returning a reference from a function -- the local data is dropped when the function returns. Return an owned type (`String`, `Vec<T>`) instead of a reference.
  • Thinking `'static` means immutable or "lives forever" -- `&'static str` means the reference is valid forever, but `T: 'static` just means T doesn't contain short-lived borrows. An owned `String` satisfies `'static`.
  • `error[E0716]` temporary value dropped while borrowed -- a temporary created in an expression is dropped at the semicolon, but a reference to it escapes. Bind the temporary to a `let` variable first.
  • Assuming lifetime annotations change how long data lives -- they don't. Annotations only describe existing relationships so the compiler can verify them.

Rust References & Lifetimes: Lifetime Annotations & Elision FAQ

What are the lifetime elision rules?

1) Each reference parameter gets its own lifetime. 2) If there's exactly one input lifetime, it's assigned to all output lifetimes. 3) If there's a `&self` or `&mut self`, its lifetime is assigned to all output lifetimes.

What does 'static mean?

`'static` means the reference is valid for the entire program duration. String literals (`"hello"`) have type `&'static str`. It doesn't mean the data is immutable; it means it lives forever.

Why do structs with references need lifetime annotations?

The struct can't outlive the data it references. The lifetime annotation tells the compiler: "this struct is only valid as long as the referenced data."

Can I have multiple lifetime parameters?

Yes! `fn foo<'a, 'b>(x: &'a str, y: &'b str)` has two independent lifetimes. You need multiple when the references have different validity periods.

What does the error 'lifetime may not live long enough' mean?

A reference might outlive its data. Usually means you're returning a reference to something that will be dropped, or your lifetime annotations don't match the actual data flow.

What is the difference between &'static T and T: 'static?

`&'static T` is a reference valid for the entire program (like string literals). `T: 'static` means T contains no non-'static borrows — it's self-contained. An owned `String` satisfies `T: 'static` even though it's not a reference.

How do I fix E0716 (temporary value dropped while borrowed)?

Bind the temporary to a named variable. Instead of `let s: &str = &String::from("hello");`, write `let owned = String::from("hello"); let s: &str = &owned;`. The variable extends the temporary's lifetime.

Rust References & Lifetimes: Lifetime Annotations & Elision Syntax Quick Reference

Basic lifetime annotation
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
Static lifetime
let s: &'static str = "hello world";
Struct with lifetime
struct Excerpt<'a> {
    part: &'a str,
}
Impl with lifetime
impl<'a> Excerpt<'a> {
    fn level(&self) -> i32 { 3 }
}
Multiple lifetimes
fn foo<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
    x
}
Lifetime outlives
fn foo<'a, 'b>(x: &'a str, y: &'b str) -> &'a str
where 'a: 'b { x }
T: 'static bound
fn spawn<T: Send + 'static>(val: T) { }
Elision (no annotation needed)
fn first_word(s: &str) -> &str {
    &s[..1]
}

Rust References & Lifetimes: Lifetime Annotations & Elision Sample Exercises

Example 1Difficulty: 2/5

Fill in the variable to dereference the reference.

r
Example 2Difficulty: 3/5

Fill in the lifetime annotation for the return type.

&'a
Example 3Difficulty: 3/5

Fill in the parameter type so Rust can elide the lifetime.

&str

+ 35 more exercises

Quick Reference
Rust References & Lifetimes: Lifetime Annotations & Elision Cheat Sheet →

Copy-ready syntax examples for quick lookup

Further Reading

  • Rust Newtype Pattern: Catch Unit Bugs at Compile Time18 min read

Start practicing Rust References & Lifetimes: Lifetime Annotations & Elision

Free daily exercises with spaced repetition. No credit card required.

← Back to Rust Syntax Practice
Syntax Cache

Build syntax muscle memory with spaced repetition.

Product

  • Pricing
  • Our Method
  • Daily Practice
  • Design Patterns
  • Interview Prep

Resources

  • Blog
  • Compare
  • Cheat Sheets
  • Vibe Coding
  • Muscle Memory

Languages

  • Python
  • JavaScript
  • TypeScript
  • Rust
  • SQL
  • GDScript

Legal

  • Terms
  • Privacy
  • Contact

© 2026 Syntax Cache

Cancel anytime in 2 clicks. Keep access until the end of your billing period.

No refunds for partial billing periods.