Skip to content

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

FixDevs ·

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.

Most of the time, Rust infers lifetimes automatically (lifetime elision). But when there are multiple input references and an output reference, Rust cannot determine which input lifetime the output should have.

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.

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.

For borrow checker errors, see Fix: Rust cannot borrow as mutable. For general Rust borrow checker issues, see Fix: Rust borrow checker error.

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