The reference trap
You write a function to extract the first word from a sentence. You create a String inside the function, find the word, and try to return a reference to it. The compiler rejects you with E0515 (cannot return value referencing local variable). The data is right there. It looks valid. Rust says no.
You stare at the error. The string exists. You can print it. Why can't you hand back a reference?
The problem isn't the syntax. The problem is time. A reference is a lease on data owned by someone else. If the owner drops the data, the reference becomes a dangling pointer. Dangling pointers cause crashes and security vulnerabilities. Rust forbids them at compile time. You can return a reference only if the data outlives the function call.
References are leases, not copies
Think of a reference like a library card that points to a specific book on a shelf. The card doesn't contain the book. It tells you where the book is. If the library burns down, the card is useless. It points to ash.
Rust checks the lease duration before handing you the card. If the book is scheduled to be destroyed at the end of the function, Rust won't let you return the card. The caller would get a card pointing to a destroyed book.
This rule forces a specific pattern. To return a reference, the data must exist outside the function. In practice, this almost always means the caller gave you the data. The caller hands you a String or a slice. You hand back a view into that same data. The caller still owns the data. The lease remains valid.
The golden rule: data must outlive the function
You can return a reference from a function when the referenced data is owned by the caller or lives longer than the function. You cannot return a reference to data created inside the function.
This sounds restrictive. It isn't. Most functions that return references are just slicing, searching, or looking up data provided by the caller. The function acts as a lens. It finds the right piece and hands back a view. No allocation happens. No data moves. The function is cheap.
Counter-intuitive but true: returning a reference is often faster than returning a value because it avoids copying. A reference is just a pointer and a length. Copying a reference is a single CPU instruction. Copying a string involves heap allocation and memory copy.
Minimal example
Here is the standard pattern. The function accepts a string slice and returns a string slice. The slice points into the original data.
/// Returns a reference to the first word in the slice.
/// The reference borrows from the input, so no allocation happens.
fn first_word(s: &str) -> &str {
// Iterate over bytes to find the space.
// Using bytes avoids Unicode complexity for this simple check.
for (i, &byte) in s.as_bytes().iter().enumerate() {
if byte == b' ' {
// Return a slice of the original string.
// This slice points into `s`, so it lives as long as `s`.
return &s[..i];
}
}
// No space found; return the whole string.
s
}
fn main() {
let my_string = String::from("hello world");
// `my_string` owns the data.
// `first_word` returns a view into `my_string`.
let word = first_word(&my_string);
println!("First word: {}", word);
// `word` is valid as long as `my_string` is valid.
}
Convention aside: &str is the slice type. It represents a view into string data. String is the owned type. When you see &str in a signature, it means "I need to read some text, but I don't care who owns it." This accepts &String, &str, and string literals. It's the most flexible way to accept text.
How the compiler connects the dots
The signature fn first_word(s: &str) -> &str does heavy lifting. You didn't write any lifetime annotations. The compiler filled them in using lifetime elision rules.
The compiler sees one input reference and one output reference. It ties the output lifetime to the input lifetime. The returned slice lives as long as s lives. If s is dropped, the slice becomes invalid, and the compiler prevents you from using it.
This connection is automatic. You don't need to write fn first_word<'a>(s: &'a str) -> &'a str. The elision rule handles it. You only need explicit lifetimes when the connection isn't obvious, like when there are multiple input references and the compiler can't guess which one the output ties to.
Trust the borrow checker here. If the compiler accepts the signature, the lifetimes are safe. The output reference is anchored to the input. The data won't disappear.
Realistic example: Config lookup
Real code often stores data in a struct and returns references from methods. This pattern is common in configuration parsers, caches, and databases.
use std::collections::HashMap;
/// A simple configuration store.
struct Config {
values: HashMap<String, String>,
}
impl Config {
/// Looks up a value by key.
/// Returns a reference to the stored string, avoiding a clone.
fn get(&self, key: &str) -> Option<&str> {
// `self.values.get` returns `Option<&String>`.
// We map it to `Option<&str>` using `as_str`.
self.values.get(key).map(|s| s.as_str())
}
}
fn main() {
let mut config = Config {
values: HashMap::new(),
};
config.values.insert("theme".to_string(), "dark".to_string());
// `config` owns the data.
// `get` returns a reference into `config`.
if let Some(theme) = config.get("theme") {
println!("Theme is: {}", theme);
}
// `theme` is valid as long as `config` is valid.
}
Convention aside: When you have a String and need a &str, call as_str(). You can also use &*s or &s[..], but as_str() signals intent clearly. It reads like "give me a view of this string." The community prefers as_str() for readability.
Pitfalls and compiler errors
Returning references trips up beginners in predictable ways. The compiler catches these errors early.
E0515 is the most common error. It happens when you try to return a reference to a local variable.
// This will not compile.
fn bad_return() -> &str {
let s = String::from("hello");
// `s` is dropped at the end of this function.
// Returning a reference here would be a dangling pointer.
&s
}
The compiler rejects this with E0515. The data dies when the function returns. The reference would point to freed memory. Rust blocks this.
E0502 appears when you try to return a reference while also borrowing the data mutably. You can't have a mutable borrow and an immutable borrow at the same time.
fn update_and_get(mut data: Vec<String>) -> &str {
data.push("new".to_string());
// Error E0502: cannot borrow `data` as immutable because it is also borrowed as mutable.
&data[0]
}
The mutable borrow from push conflicts with the immutable borrow from the return. You need to finish the mutation before returning the reference.
Another pitfall is using &String in function signatures. If you write fn foo(s: &String), you force callers to own a String. &str accepts &String, &str, and string literals. Always prefer &str in function signatures. It's the community standard for a reason.
Don't fight the compiler here. If you need to return new data, return a String. If you need to return existing data, return a &str. The types tell the story.
Decision matrix
Pick the right return type based on ownership and lifetime requirements.
Use &str when the data is owned by the caller and you only need to read or pass it along. This avoids allocation and keeps the caller in control. Use String when the function creates new data or modifies the input and must return ownership of the result. This transfers ownership to the caller and frees the function from lifetime constraints. Use &'static str when the string is hardcoded in the source and lives for the entire program duration. This is safe because the data never drops. Use Cow<str> when you want to accept both owned and borrowed data and return a result that might be either, deferring allocation until a mutation is required. This unifies the API and optimizes the common case.
Treat the return type as a contract. &str promises the data lives elsewhere. String promises you own the data. &'static str promises the data lives forever. Pick the contract that matches your needs.