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.