Skip to content
← All articles

Returning a reference to a local — the E0515 wall

The error where people conclude Rust won't let them write functions. Naming the exact impossibility — the caller picks `'a`, so no local can satisfy it — dissolves it, and the four real fixes are a genuine API-design lesson.

This is the error at which a certain number of people quietly decide that Rust is not for them.

fn longest<'a>(x: &str, y: &str) -> &'a str {
    let result = String::from("really long string");
    result.as_str()
}
error[E0515]: cannot return value referencing local variable `result`
              returns a value referencing data owned by the current function

It looks like the compiler is refusing to let you return a string. It is not. It is refusing to let you return a dangling pointer, and the reason no annotation can help is precise and worth stating exactly.

Why no annotation saves it

Look at where 'a comes from. It is a parameter of the function, which means the caller chooses it. Every call site instantiates 'a with whatever region that caller needs — possibly a very long one:

let winner: &str;
{
    let a = String::from("aaa");
    winner = longest(&a, "bbb");   // caller wants 'a to reach past this block
}
println!("{winner}");

Your body must produce a reference valid for whatever the caller picked. But result is a local: it is dropped at the closing brace of longest. There is no region containing that } and also containing the caller’s use, so there is no possible value of 'a your body can satisfy.

That is the whole story. It is not a limitation of the checker, not a conservatism, not something a smarter compiler would allow. A function cannot return a reference to storage it owns, because that storage is gone by the time the caller looks. Every language has this rule; most of them enforce it with a crash six weeks later.

💡Try to "fix" it by writing -> &'static str instead of -> &'a str. What happens, and why is this attempt so common? click to reveal
fn shout(s: &str) -> &'static str {
    let out = s.to_uppercase();
    out.as_str()
}

Exactly the same error:

error[E0515]: cannot return value referencing local variable `out`

'static is not a stronger version of 'a in the sense people hope. It is the largest region — the whole program — so if a caller-chosen 'a was impossible to satisfy, 'static is more impossible. You have made the promise harder, not the code better.

Why is the attempt so common? Because learners have usually seen the compiler suggest 'static for a different error. E0106 on fn get_str() -> &str really does suggest it, and there it can be right (a literal genuinely is 'static). So 'static gets filed away as “the thing you write when it complains about lifetimes”, and the next time anything complains about lifetimes, out it comes.

The reliable tell: 'static is right when the data is already static — a literal, a const, a static. It is never right as a way to make freshly-allocated data live longer.

The four real fixes

Each is a genuine API decision with a genuine cost. Picking between them is the actual lesson.

1. Return an owned String

fn shout(s: &str) -> String {
    let mut out = s.to_uppercase();
    out.push('!');
    out
}

Simplest, and right most of the time. Cost: one allocation per call, and the caller cannot avoid it even when they only wanted to look at the result.

2. Return Cow<'_, str>

When the answer is sometimes a slice of the input and sometimes new data, Cow (“clone on write”) lets you say so:

use std::borrow::Cow;

pub fn normalize(s: &str) -> Cow<'_, str> {
    if s.contains(' ') {
        Cow::Owned(s.replace(' ', "_"))
    } else {
        Cow::Borrowed(s)
    }
}

Cost: the type is in your public API forever, and callers have to think about it. Benefit: the common case — nothing to change — allocates nothing. This is what str::trim would return if it ever had to modify anything, and it is the right answer far more often than its usage rate suggests.

3. Take an output buffer

pub fn shout_into(s: &str, out: &mut String) {
    out.clear();
    out.push_str(&s.to_uppercase());
    out.push('!');
}

Cost: an uglier signature, and the caller has to manage the buffer. Benefit: a caller in a loop allocates once instead of a million times. This is the standard shape in hot paths and in no_std code, and it is why so much of the standard library has _into/_to variants.

4. Restructure so the caller owns the storage

Often the best fix is the one that makes the question disappear: do not create the data inside the function at all.

// instead of building a String and returning a slice of it,
// return a slice of something the caller already has
pub fn first_word(s: &str) -> &str {
    match s.find(' ') {
        Some(i) => &s[..i],
        None => s,
    }
}

Cost: you have to be able to express the answer as a view of the input. Benefit: zero allocation, zero lifetime gymnastics, and the borrow checker becomes an ally instead of an obstacle. When a function can be written this way, it usually should be.

💡A function parses a line of key=value and needs to return the key and the value. Which of the four fixes fits, and what does that tell you about the general shape of good Rust APIs? click to reveal
pub fn split_pair(line: &str) -> (?, ?)

Fix 4, and it barely needs discussing once you see it:

pub fn split_pair(line: &str) -> (&str, &str) {
    line.split_once('=').unwrap_or((line, ""))
}

Both halves are already sitting in line. There is nothing to build, so there is nothing to own, so there is nothing to return by value. Elision handles the annotations (one lifetime-carrying parameter, two elided outputs, rule 2 fires) and you never write a 'a.

The general shape: prefer producing views of your input; allocate only when the answer contains bytes that did not exist before. Uppercasing invents bytes. Trimming does not. Splitting does not. Replacing does — sometimes — which is precisely the Cow case.

When you find yourself fighting E0515, the first question is not “which annotation” but “am I building data I did not need to build?” A surprising fraction of the time the answer is yes, and the fight ends.

One fix you should know about and mostly not use

fn leaky(s: &str) -> &'static str {
    Box::leak(s.to_uppercase().into_boxed_str())
}

This compiles. Box::leak gives up ownership of a heap allocation and hands you a &'static mut to it, which really is valid for the rest of the program — because it is never freed.

That is not a trick or a loophole; it is a deliberate, permanent memory leak, and it is a legitimate tool in exactly one situation: initialising something once at startup that must live for the program’s life anyway (a parsed config, an interned string table). Call it in a loop and you have written a slow program that eventually dies.

Anyone who shows you Box::leak as “the fix for E0515” without saying the word leak is doing you a disservice.

The sibling error: E0716

E0515 is about returning a reference to a local. Its close relative is about holding a reference to a temporary:

fn bar(s: &String) -> &String { s }
fn foo() -> String { String::from("hi") }

fn main() {
    let r = bar(&foo());
    // error[E0716]: temporary value dropped while borrowed
    println!("{r}");
}

foo() produces a temporary with no name. It is dropped at the end of the statement, and r outlives it. The fix is to give it a name:

let owned = foo();
let r = bar(&owned);

Curiously, let r = &foo(); on its own is fine — temporary lifetime extension applies to a direct &-of-temporary in a let, but stops the moment you route it through a function call. And edition 2024 — which is what this site compiles with — changed two of the rules about when temporaries drop, so pre-2025 advice about E0716 can be subtly wrong. Those changes get their own article; for now, just know that “when exactly does this temporary die” is edition-dependent and worth checking rather than assuming.

The one-line summary

The caller picks 'a, so no local can satisfy it. When you hit E0515, stop looking for an annotation and start choosing between: own the result, Cow it, write into the caller’s buffer, or find a way to return a view of the input.