The error that stops dangling pointers
You write a function that builds a message and returns it. The compiler stops you with E0515: "cannot return reference to local variable". You look at the code. The variable is right there. It has data. Why can't you return it?
The answer lies in how Rust manages memory when a function finishes. Rust refuses to hand you a reference to data that will be destroyed the moment the function returns. E0515 is the compiler's way of preventing a dangling pointer before your code ever runs.
Why local variables vanish
Every function call creates a stack frame. Local variables live inside that frame. When the function returns, the stack frame is popped. The memory for those variables is reclaimed instantly. If you return a reference to a local variable, you are returning a pointer to memory that no longer holds your data.
Think of a function as a pop-up shop that operates for one hour. Local variables are the merchandise inside. When the hour ends, the shop packs up and leaves. Returning a reference is like giving a customer a map to a specific shelf in that shop after it's gone. The map points to an empty lot.
Rust catches this at compile time. In languages without this check, the program might run for a while, then crash randomly when another function overwrites the reclaimed memory. Rust eliminates that class of bugs entirely.
The stack frame demolition
The stack is designed for speed. Allocation and deallocation happen by moving a pointer. No complex bookkeeping is needed. This speed comes with a constraint: memory is only valid while the stack frame exists.
fn get_message() -> &str {
let msg = String::from("Hello");
// WHY: msg is created on the heap, but the String struct lives on the stack.
// WHY: The String struct owns the heap memory.
&msg
// WHY: We try to return a reference to the String struct.
// WHY: The compiler knows the String struct will be dropped when this function returns.
// WHY: Returning &msg would leave the caller with a reference to dropped memory.
// ERROR: E0515 cannot return reference to local variable `msg`
}
The error message is precise. msg is local. It cannot outlive the function. The compiler rejects the code because the reference would dangle.
Fix 1: Return ownership with String
The most common fix is to return an owned value instead of a reference. String owns its data on the heap. When you return a String, you move ownership to the caller. The heap memory stays alive because the caller now owns it.
/// Returns a greeting message owned by the caller.
fn get_greeting() -> String {
let msg = String::from("Hello");
msg
// WHY: Returning msg moves ownership to the caller.
// WHY: The String struct and its heap data are transferred.
// WHY: The caller decides when to drop the memory.
}
fn main() {
let s = get_greeting();
println!("{s}");
// WHY: s owns the String. It lives until the end of main.
}
This pattern is idiomatic. Functions that create new data typically return owned types. The caller gets full control over the lifetime.
Fix 2: Return a reference to input data
If the data comes from the caller, you can return a reference to that data. The reference borrows the input, so it lives as long as the input does.
/// Returns a reference to the input string.
fn get_greeting<'a>(input: &'a str) -> &'a str {
input
// WHY: We return a reference to the input.
// WHY: The lifetime 'a ties the output to the input.
// WHY: The reference is valid as long as input is valid.
}
fn main() {
let input = String::from("Hello");
let s = get_greeting(&input);
println!("{s}");
// WHY: s borrows from input. It cannot outlive input.
}
This avoids allocation. The function just passes through a reference. Use this when the output is derived from the input and you want to avoid copying data.
Fix 3: Return a static string
If the data is a hardcoded constant, you can return a &'static str. Static strings live for the entire duration of the program. They are stored in the binary, not on the stack or heap.
/// Returns a static greeting.
fn get_static_greeting() -> &'static str {
"Hello"
// WHY: String literals have type &'static str.
// WHY: They live in the read-only data segment of the binary.
// WHY: They never get dropped, so the reference is always valid.
}
This is efficient. No allocation occurs. The reference is safe because the data never goes away.
Why lifetimes won't save you here
A common misconception is that adding a lifetime parameter fixes E0515. It does not. Lifetimes describe relationships between references. They do not extend the lifetime of a variable.
fn bad_lifetime<'a>() -> &'a str {
let s = String::from("data");
&s
// WHY: The lifetime 'a describes the reference, not the variable s.
// WHY: s is still local and will be dropped when the function returns.
// WHY: Adding 'a does not make s live longer.
// ERROR: E0515 cannot return reference to local variable `s`
}
The compiler sees that s is dropped. The reference points to s. The reference would dangle. The lifetime parameter is irrelevant. You cannot sign a contract saying "This reference lives forever" if the underlying data is destroyed. Lifetimes describe reality. They do not change it.
The "Accept slice, return owned" convention
Functions that transform text typically accept &str and return String. This pattern appears everywhere in the standard library. It lets callers pass string slices without cloning. The function owns the result.
/// Formats a user status message.
fn format_status(user: &str, active: bool) -> String {
let status = if active { "online" } else { "offline" };
format!("User {} is {}", user, status)
// WHY: format! returns a String.
// WHY: The function owns the result.
// WHY: The caller can store it, pass it, or drop it.
}
This convention balances flexibility and safety. Callers can pass &str from String, &str literals, or any type that derefs to &str. The function returns a String that the caller owns.
What happens in unsafe
Rust prevents E0515 by default. If you use unsafe, you can bypass the check. You can cast a reference to a raw pointer, return it, and dereference it later. The program will likely crash or produce garbage data.
fn dangerous() -> *const str {
let s = String::from("hello");
let ptr = s.as_str() as *const str;
ptr
// WHY: We return a raw pointer to the String's data.
// WHY: The String s is dropped when the function returns.
// WHY: The heap memory is freed.
// WHY: ptr now points to freed memory.
}
fn main() {
let ptr = dangerous();
// WHY: Dereferencing ptr is undefined behavior.
// WHY: The memory may have been reused or corrupted.
// WHY: This can crash the program or leak data.
// SAFETY: This code is intentionally unsafe to demonstrate the danger.
// 1. ptr points to memory that has been freed.
// 2. Accessing ptr violates memory safety.
// 3. This is undefined behavior.
let _data = unsafe { &*ptr };
}
E0515 exists to stop this. The compiler forces you to return owned data or valid references. Trust the error. It is protecting you from undefined behavior that would be impossible to debug.
Decision matrix
Use String when the data is created inside the function or depends on local computation. The caller gets ownership and decides when to drop it.
Use &'static str when the data is a hardcoded constant that lives for the entire program duration. This avoids allocation entirely.
Use a reference to input data when the output is derived from the caller's data and you want to avoid allocation. The lifetime ties the output to the input.
Use unsafe only when you are implementing a low-level abstraction and can prove that the pointer remains valid. This is rare and requires rigorous documentation.
Don't fight the compiler here. Return the owned value.