Fix Rust 'cannot return value referencing local variable' (E0515)

intermediate๐Ÿฆ€ Rust2026-06-01| Rust (any version), rustc, Cargo โ€” all platforms (Linux, macOS, Windows)

Error Message

error[E0515]: cannot return value referencing local variable `result`
#rust#lifetime#reference#borrow#return

The Error

error[E0515]: cannot return value referencing local variable `result`
 --> src/main.rs:5:5
  |
5 |     &result
  |     ^------
  |     ||
  |     |`result` is borrowed here
  |     returns a value referencing data owned by the current function

This error means you tried to return a reference to something the function itself owns. The moment the function returns, that local variable is dropped โ€” so any reference to it would immediately point to freed memory. Rust's borrow checker catches this before your code ever runs.

Root Cause

A local variable dies when its function returns. That's it. Rust's rule is simple: a reference can't outlive the data it points to. Return a reference to a local, and you'd get a dangling pointer โ€” the kind of bug that crashes C programs and corrupts memory silently. Rust just refuses to compile it.

The classic trigger looks like this:

fn get_greeting(name: &str) -> &str {
    let result = format!("Hello, {}!", name); // result is a local String
    &result // ERROR: result is dropped here, reference would dangle
}

The compiler error:

error[E0515]: cannot return value referencing local variable `result`
 --> src/main.rs:3:5
  |
3 |     &result
  |     ^------
  |     |`result` is borrowed here
  |     returns a value referencing data owned by the current function

Step-by-Step Fixes

Fix 1: Return the Owned Type Instead of a Reference

If the function creates data, just return it. No reference needed.

// Before (broken)
fn get_greeting(name: &str) -> &str {
    let result = format!("Hello, {}!", name);
    &result // E0515
}

// After (fixed)
fn get_greeting(name: &str) -> String {
    format!("Hello, {}!", name) // Return the owned String directly
}

Same pattern applies to collections:

// Before (broken)
fn even_numbers(input: &[i32]) -> &[i32] {
    let result: Vec<i32> = input.iter().filter(|&&x| x % 2 == 0).cloned().collect();
    &result // E0515
}

// After (fixed)
fn even_numbers(input: &[i32]) -> Vec<i32> {
    input.iter().filter(|&&x| x % 2 == 0).cloned().collect()
}

Fix 2: Return a Reference to the Input Parameter

Not building new data? Just selecting or slicing from the input? Reference the input directly โ€” no local copy needed.

// Before (broken โ€” creates local copy unnecessarily)
fn first_word(s: &str) -> &str {
    let owned = s.to_string(); // unnecessary local variable
    owned.split_whitespace().next().unwrap_or(&owned) // E0515
}

// After (fixed โ€” reference the input directly)
fn first_word(s: &str) -> &str {
    s.split_whitespace().next().unwrap_or("")
    // lifetime of return value tied to `s`, which is fine
}

Fix 3: Use Cow<'_, str> When You Need Both

Some functions conditionally return either a borrowed value or a freshly created one. Cow (Clone-on-Write) is built exactly for this case:

use std::borrow::Cow;

fn sanitize(input: &str) -> Cow<'_, str> {
    if input.contains('<') {
        Cow::Owned(input.replace('<', "&lt;")) // newly created String
    } else {
        Cow::Borrowed(input) // just borrow the original
    }
}

fn main() {
    let clean = sanitize("hello <world>");
    println!("{}", clean); // hello &lt;world>
}

Fix 4: Restructure to Avoid Building Data Inside the Function

Sometimes the real fix is deleting unnecessary allocation. A function that builds a local String just to slice it can often work directly on the &str input instead:

// Before (broken)
fn get_domain(url: &str) -> &str {
    let trimmed = url.trim_start_matches("https://").to_string(); // local String
    trimmed.split('/').next().unwrap_or("") // E0515
}

// After โ€” trim directly on the &str input, no local String needed
fn get_domain(url: &str) -> &str {
    url.trim_start_matches("https://")
       .trim_start_matches("http://")
       .split('/')
       .next()
       .unwrap_or("")
}

Verify the Fix

Run cargo build. If E0515 is gone, you're done. Also run cargo test to confirm nothing broke:

cargo build
cargo test

For a faster iteration loop, cargo check skips code generation entirely and catches type and lifetime errors in roughly half the time of a full build:

cargo check

Quick Decision Guide

  • Building new data inside the function? โ†’ Return the owned type (String, Vec<T>, etc.)
  • Just selecting or slicing from the input? โ†’ Return a reference to the input parameter
  • Sometimes building, sometimes not? โ†’ Use Cow<'_, str> or Cow<'_, [T]>
  • Caller needs to mutate the result? โ†’ Always return the owned type

Common Mistakes

Adding lifetime annotations does not fix this. It's a very common attempt:

// Still broken โ€” lifetime annotation doesn't extend variable lifetime
fn get_greeting<'a>(name: &'a str) -> &'a str {
    let result = format!("Hello, {}!", name);
    &result // E0515 โ€” the annotation can't save a local variable
}

Lifetime annotations describe relationships between existing lifetimes. They don't create new ones or extend a local variable's scope. The data has to physically outlive the reference โ€” no annotation changes that.

Related Error Notes