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.

missing lifetime specifier / may not live long enough

The first time I really understood lifetimes, I realized I had them backwards: I thought 'a made a reference live longer, when in fact it changes nothing at runtime and only describes a relationship the compiler then checks. I learned to read missing lifetime specifier as a fair question rather than an obstacle, which input does this returned reference come from, and how does the compiler know the data outlives the reference? In my experience, once you answer that ownership question instead of fighting the annotation, the fix is almost always one of three moves: own the data, share it with Arc/Rc, or reshape the API so the lifetimes line up by themselves.

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.

What a Lifetime Annotation Actually Says

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 (stabilized Feb 2025 with Rust 1.85) shipped let-chains, refined unsafe boundaries, and, most relevant to lifetimes, new RPIT capture rules: a return-position impl Trait now captures all in-scope lifetime and type parameters by default, with an opt-out + use<...> bound to restrict them. That flips which + 'a annotations you need on impl Trait returns depending on your edition (see the last item below).

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

It bears repeating because it is the crux: writing 'a never extends how long anything lives. It states a relationship, “the output borrows from this input”, that the compiler then verifies. If the relationship you assert is false, the annotation does not paper over it; the compiler rejects the annotated code too. Treat lifetimes as machine-checked documentation, not as a lever you pull to make an error go away.

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
}

The reflex I try to catch in myself is reaching for a 'a parameter on a struct when the honest fix is to own the data. A struct that borrows (&'a str, &'a [T]) is chained to its source, it cannot outlive whatever it points into, which is exactly what you do not want for anything long-lived. Store String and Vec<T> instead, and reserve borrowing structs for short-lived views where tying the two lifetimes together is genuinely what you mean.

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. Cow earns its place when some code paths can borrow and others must produce a fresh, owned value. It borrows in the common case (zero allocation) and only allocates when a branch genuinely creates new data that has nothing to borrow from:

use std::borrow::Cow;

fn without_spaces(input: &str) -> Cow<'_, str> {
    if input.contains(' ') {
        Cow::Owned(input.replace(' ', ""))  // transformed: a new String, must own
    } else {
        Cow::Borrowed(input)                // unchanged: borrow, no allocation
    }
}

Always returning String here (the previous fix) would allocate even when the input needed no change; Cow skips that allocation on the common path.

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

Lifetime Errors That Run Deeper

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 non-scoped thread, sending a value to a spawned thread over an mpsc::channel, or storing a closure in a global OnceCell all require 'static. (The channel itself does not impose 'static, the receiving 'static thread does, which is why std::thread::scope lets you send borrowed data.) If your closure captures &local_var, you must either own the data (String, Vec<T>) or use Arc<T>. The compiler often reports it as “borrowed value does not live long enough” when the real requirement 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 captures the right lifetimes. This is edition-dependent. On editions before 2024, a function returning impl Iterator<Item = &'a T> often needs an explicit + 'a bound, and omitting it produces “hidden type captures lifetime that does not appear in bounds.” On the 2024 edition, RPIT captures every in-scope lifetime automatically, so the + 'a is usually unnecessary, and your problem flips: to stop capturing a lifetime you add a + use<...> bound listing only the parameters you want captured.

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