Skip to content

Fix: Rust lifetime may not live long enough / missing lifetime specifier

FixDevs · (Updated: )

Part of:  Go, Rust & Systems Errors

Quick Answer

How to fix Rust lifetime errors including missing lifetime specifier, may not live long enough, borrowed value does not live long enough, and dangling references.

The Error

You compile Rust code and get:

error[E0106]: missing lifetime specifier
 --> src/main.rs:3:24
  |
3 | fn first_word(s: &str) -> &str {
  |                  ----     ^ expected named lifetime parameter

Or variations:

error: lifetime may not live long enough
 --> src/main.rs:8:5
  |
7 | fn longest(x: &str, y: &str) -> &str {
  |               -         - let's call the lifetime of this reference `'1`
  |               |
  |               let's call the lifetime of this reference `'2`
8 |     if x.len() > y.len() { x } else { y }
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ method was supposed to return data with lifetime `'2` but it is returning data with lifetime `'1`
error[E0597]: borrowed value does not live long enough
error[E0515]: cannot return reference to local variable

Rust’s borrow checker cannot prove that a reference will be valid for as long as it is used. You need to add lifetime annotations to help the compiler understand the relationship between references.

Why This Happens

Every reference in Rust has a lifetime — the scope during which the reference is valid. The borrow checker ensures you never use a reference after the data it points to has been dropped. Lifetimes are not runtime data; they are a static-analysis label that tells the compiler “this reference is valid for at most as long as that variable.” When the compiler cannot prove the relationship is sound, it refuses to compile rather than risk a dangling pointer.

Most of the time, Rust infers lifetimes automatically (lifetime elision). The elision rules cover the common cases: a function with one reference parameter, methods that take &self, and references that flow straight through. But when there are multiple input references and an output reference, the rules give up — there is no single “obvious” answer for which input the output is tied to, so you must annotate.

Lifetime errors are not a Rust quirk to be silenced. They surface real ownership questions: who owns this data, when does it die, who is allowed to look at it? Adding 'a annotations does not extend lifetimes — it only describes the relationships that must hold. If the relationships you describe are wrong, the compiler will reject your annotation too. The fix is usually one of three: tighten ownership (own the data), broaden ownership (use Arc or Rc), or shape your API so the lifetimes naturally line up.

Common causes:

  • Returning a reference from a function with multiple reference parameters.
  • Storing a reference in a struct without specifying how long it lives.
  • Returning a reference to a local variable (always invalid — the data is dropped when the function ends).
  • Reference outlives the data it borrows due to scope issues.

Version History That Changes the Failure Mode

The borrow checker is one of the most actively improved parts of the Rust compiler. Errors that required ugly workarounds in 2017 are accepted unchanged by rustc in 2024. Knowing your rustc --version and edition matters.

  • Non-Lexical Lifetimes (NLL) stabilized in Rust 2018 edition (Dec 6, 2018, with Rust 1.31). Before NLL, a borrow lived until the end of its enclosing block; afterward, borrows end at their last use. Code like let mut v = vec![1]; let x = &v[0]; v.push(2); only compiles after NLL because the immutable borrow x ends before the push, if x is not used afterward.
  • Rust 2021 edition (Oct 2021, with Rust 1.56) introduced disjoint captures in closures. A closure that touches s.field_a no longer holds a borrow on the whole s, so you can borrow s.field_b mutably alongside it. Before 2021, you had to manually destructure.
  • Rust 1.65 (Nov 3, 2022) stabilized Generic Associated Types (GATs). GATs let traits express lifetimes that depend on method calls (type Item<'a> inside a trait), which previously required ugly workarounds with for<'a> HRTBs.
  • Rust 1.65 also stabilized let ... else, which dramatically improves “borrow until the bind, then drop” patterns.
  • Rust 1.75 (Dec 28, 2023) stabilized async fn in traits and return-position impl Trait in traits (RPITIT). These shifts changed which lifetime annotations are required on async-heavy trait code.
  • Rust 1.76+ (Feb 2024 onward) progressively improved diagnostics for 'static bounds. The hint about “consider adding 'a: outlives bound” now points to the exact missing annotation more often.
  • Rust 1.79 (Jun 13, 2024) stabilized inline const expressions and continued improvements to lifetime elision in impl blocks.
  • Polonius (the next-generation borrow checker, started 2018) has been gradually folded into the compiler under -Z polonius. It accepts more code, especially patterns with conditional returns of references. Watch the release notes; some borrow errors are slated to disappear without code changes.
  • Rust 2024 edition (planned 2024) brings let-chains stabilization, RPIT lifetime capture rules, and refined unsafe boundaries — none change borrow checking semantics, but they affect how concisely you can write conditional borrow checks.

If you are on Rust < 1.31, upgrade before debugging — NLL alone fixes a large fraction of the lifetime errors that older guides discuss.

Fix 1: Add Lifetime Annotations

When the compiler asks for a lifetime specifier, add one:

Broken:

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

Fixed — add lifetime annotation 'a:

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

The 'a says: “The returned reference lives at least as long as both input references.” The compiler uses this to verify that callers do not use the result after either input is dropped.

Usage:

let result;
let string1 = String::from("hello");
{
    let string2 = String::from("world");
    result = longest(&string1, &string2);
    println!("{}", result);  // OK — both strings are alive
}
// println!("{}", result);  // Error! string2 is dropped, result might point to it

Pro Tip: Lifetime annotations do not change how long data lives. They describe the relationship between lifetimes of references so the compiler can verify safety. Think of them as documentation that the compiler checks.

Fix 2: Fix Struct Lifetime Annotations

Structs that hold references need lifetime parameters:

Broken:

struct Excerpt {
    text: &str,  // Error: missing lifetime specifier
}

Fixed:

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

impl<'a> Excerpt<'a> {
    fn new(text: &'a str) -> Self {
        Excerpt { text }
    }

    fn text(&self) -> &str {
        self.text
    }
}

Usage — the struct cannot outlive the data it references:

let excerpt;
{
    let novel = String::from("Call me Ishmael. Some years ago...");
    excerpt = Excerpt::new(&novel);
    println!("{}", excerpt.text);  // OK
}
// println!("{}", excerpt.text);  // Error! novel is dropped

Alternative — own the data instead:

struct Excerpt {
    text: String,  // Owns the data, no lifetime needed
}

Common Mistake: Adding lifetime parameters to structs when you should just own the data. If the struct needs to live independently of its data source, use String instead of &str, Vec<T> instead of &[T], etc. Only use references in structs when you intentionally want to borrow data temporarily.

Fix 3: Fix “Cannot Return Reference to Local Variable”

You can never return a reference to data created inside the function:

Broken:

fn create_greeting(name: &str) -> &str {
    let greeting = format!("Hello, {}!", name);
    &greeting  // Error: cannot return reference to local variable
}

The greeting String is dropped when the function ends. A reference to it would be dangling.

Fixed — return an owned value:

fn create_greeting(name: &str) -> String {
    format!("Hello, {}!", name)  // Return the String itself
}

Fixed — return a reference to the input (if possible):

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();
    for (i, &byte) in bytes.iter().enumerate() {
        if byte == b' ' {
            return &s[..i];  // OK — returns a part of the input
        }
    }
    s  // OK — returns the input itself
}

This works because the returned reference points to part of the input s, which outlives the function call.

Fix 4: Fix “Borrowed Value Does Not Live Long Enough”

The referenced data is dropped before the reference is last used:

Broken:

let r;
{
    let x = 5;
    r = &x;  // Error: x does not live long enough
}
println!("{}", r);  // r still in use, but x is dropped

Fixed — extend the lifetime of the data:

let x = 5;  // Move x to the outer scope
let r = &x;
println!("{}", r);  // OK — x is still alive

In closures:

// Broken — closure captures a reference to a local
fn make_printer() -> impl Fn() {
    let msg = String::from("hello");
    || println!("{}", msg)  // Error: msg doesn't live long enough
}

// Fixed — move ownership into the closure
fn make_printer() -> impl Fn() {
    let msg = String::from("hello");
    move || println!("{}", msg)  // msg is moved into the closure
}

Fix 5: Fix Multiple Lifetime Parameters

When inputs have different lifetimes:

fn first_or_default<'a, 'b>(first: &'a str, default: &'b str) -> &'a str {
    if !first.is_empty() {
        first
    } else {
        default  // Error: lifetime 'b may not live long enough
    }
}

Fixed — use the same lifetime:

fn first_or_default<'a>(first: &'a str, default: &'a str) -> &'a str {
    if !first.is_empty() { first } else { default }
}

Fixed — return owned value when lifetimes differ:

fn first_or_default(first: &str, default: &str) -> String {
    if !first.is_empty() {
        first.to_string()
    } else {
        default.to_string()
    }
}

Using Cow to avoid unnecessary cloning:

use std::borrow::Cow;

fn first_or_default<'a>(first: &'a str, default: &'a str) -> Cow<'a, str> {
    if !first.is_empty() {
        Cow::Borrowed(first)
    } else {
        Cow::Borrowed(default)
    }
}

Fix 6: Fix Lifetime Bounds on Traits

Trait objects and generic bounds with lifetimes:

Broken:

trait Summary {
    fn summarize(&self) -> String;
}

fn get_summary(item: &dyn Summary) -> &str {
    // Error: cannot determine the lifetime of the returned reference
    &item.summarize()  // Also: cannot return reference to temporary!
}

Fixed — return an owned type:

fn get_summary(item: &dyn Summary) -> String {
    item.summarize()
}

Trait objects in structs need lifetime bounds:

struct Processor<'a> {
    handler: &'a dyn Summary,
}

// Or use Box for owned trait objects (no lifetime needed)
struct Processor {
    handler: Box<dyn Summary>,
}

Lifetime bounds on generic types:

fn print_items<'a, T: 'a + std::fmt::Display>(items: &'a [T]) {
    for item in items {
        println!("{}", item);
    }
}

Fix 7: Use ‘static Lifetime

'static means the reference lives for the entire program:

// String literals are 'static
let s: &'static str = "hello world";

// Thread::spawn requires 'static because the thread may outlive the caller
std::thread::spawn(move || {
    // Everything captured must be 'static (owned or 'static references)
    println!("{}", s);
});

When to use 'static:

// Functions that return &'static str (compile-time strings)
fn greeting() -> &'static str {
    "Hello, world!"
}

// Error messages
fn error_message(code: u32) -> &'static str {
    match code {
        404 => "Not found",
        500 => "Internal error",
        _ => "Unknown error",
    }
}

T: 'static does not mean “lives forever” — it means “contains no non-static references”:

fn spawn_task<T: Send + 'static>(value: T) {
    std::thread::spawn(move || {
        use_value(value);
    });
}

// String is 'static (it owns its data)
spawn_task(String::from("hello"));  // OK

// &str with a limited lifetime is NOT 'static
let local = String::from("hello");
spawn_task(&local);  // Error: &local is not 'static
spawn_task(local);   // OK: move the String (which is 'static)

Fix 8: Lifetime Elision Rules

Rust automatically infers lifetimes in many cases. Know the rules to avoid unnecessary annotations:

Rule 1: Each reference parameter gets its own lifetime.

Rule 2: If there is exactly one input lifetime, it is assigned to all output references.

Rule 3: If one parameter is &self or &mut self, its lifetime is assigned to output references.

// No annotations needed (Rule 2 — one input reference)
fn first_word(s: &str) -> &str { ... }
// Equivalent to: fn first_word<'a>(s: &'a str) -> &'a str

// No annotations needed (Rule 3 — &self)
impl MyStruct {
    fn name(&self) -> &str { &self.name }
}
// Equivalent to: fn name<'a>(&'a self) -> &'a str

// Annotations needed (two input references, ambiguous)
fn longest(x: &str, y: &str) -> &str { ... }  // Error!
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { ... }  // Fixed

Still Not Working?

Consider using Arc or Rc for shared ownership:

use std::sync::Arc;

let data = Arc::new(String::from("shared"));
let clone = Arc::clone(&data);

std::thread::spawn(move || {
    println!("{}", clone);  // clone is 'static and Send
});

Check for Higher-Ranked Trait Bounds (HRTB):

fn apply<F>(f: F) where F: for<'a> Fn(&'a str) -> &'a str {
    let s = String::from("hello");
    println!("{}", f(&s));
}

Read the compiler’s suggestion. Rust’s error messages for lifetime issues are exceptionally detailed. The compiler often suggests exactly which lifetime annotation to add and where.

Check whether you have an unintended 'static requirement. Spawning a thread, sending a value over an mpsc::channel, or storing a closure in a global OnceCell all require 'static. If your closure captures &local_var, you must either own the data (String, Vec<T>) or use Arc<T>. The compiler reports it as “borrowed value does not live long enough” but the real issue is 'static.

Check for self-referential structs. A struct that holds both a Vec<u8> and a &[u8] slice into that Vec cannot be expressed safely with 'a annotations — once you move the struct, the slice points to freed memory. Crates like ouroboros or self_cell handle this; rolling your own with raw pointers is unsafe and usually wrong.

Check whether async is hiding a lifetime. async fn foo(x: &str) -> &str desugars to fn foo(x: &str) -> impl Future<Output = &str> + '_. The '_ is the captured lifetime of x. If you store the future and let x go out of scope, the borrow checker rejects it. Use async move blocks that take owned values when in doubt.

Check whether impl Trait in return position needs + '_. Functions returning impl Iterator<Item = &T> without an explicit + 'a bound were rejected for years; Rust 1.75 relaxed the rules but only in some positions. If you see “lifetime parameter 'a is never used,” add + 'a to the return type.

For borrow checker errors, see Fix: Rust cannot borrow as mutable. For general Rust borrow checker issues, see Fix: Rust borrow checker error. For trait-bound failures that hide lifetime problems, see Fix: Rust trait not implemented. For Result and ? operator confusion that surfaces alongside lifetime issues, see Fix: Rust error handling not working.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles