Returning a reference from a function
You're writing a text processor. You have a function that finds the first word in a sentence. You want to return that word without copying the whole string. You write fn first_word(s: String) -> &str and the compiler rejects you. Or you try fn first_word() -> &str, create a local string, and return a reference to it, and the compiler rejects you again. You're trying to hand back a pointer to data, but Rust needs to know where that data lives and how long it stays alive.
Returning a reference is one of the first walls you hit in Rust. It forces you to think about memory layout and data flow. The compiler isn't being difficult. It's protecting you from dangling pointers. Once you understand the contract between the caller and the callee, returning references becomes a natural way to write zero-copy code.
The contract of a reference
A reference &T is a pointer to data that someone else owns. When a function returns a reference, it promises that the data behind the pointer exists somewhere else and will not be dropped while the caller uses it. The function calculates the address, but it does not manage the lifetime of the data.
Think of a library card catalog. You ask the librarian for the location of a book. The librarian gives you a slip of paper with the shelf number. The book isn't on the slip. The book is on the shelf. If the library burns down, the slip is useless. Rust checks that the library isn't burning down while you hold the slip. The function is the librarian. The caller is the tourist. The data is the book. The reference is the slip.
The function can only return a reference to data that outlives the function call. This usually means the data came from the caller as an argument, or the data is stored in a struct that the caller also holds. The function cannot return a reference to a local variable. Local variables are destroyed when the function returns. A reference to a destroyed variable is a dangling pointer. Rust forbids dangling pointers at compile time.
Minimal example
Here is the standard way to return a reference. The function takes a borrowed string slice and returns a borrowed string slice.
fn first_word(s: &str) -> &str {
// s is a borrowed slice. We don't own the data.
// We can return a slice of s because s outlives this function.
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
// Return a slice pointing into s.
// The pointer part points to the same memory as s.
// The lifetime of the return value is tied to s.
return &s[0..i];
}
}
// No space found. Return the whole string.
&s[..]
}
The function takes &str. This is a slice. A slice is a fat pointer. It contains a memory address and a length. When you write &s[0..i], you create a new slice. The pointer part points to the same memory as s. The length is i. Because the pointer points into s, the compiler knows the data lives as long as s lives.
The return type is &str. Rust sees one input reference and one output reference. It applies lifetime elision. The output lifetime is connected to the input lifetime. If the caller drops s, the returned reference becomes invalid. The compiler enforces this connection. You don't need to write lifetime annotations manually. The compiler infers them.
Don't fight the borrow checker by hiding data behind references. Return the value if you own it.
How the compiler checks the lifetime
The compiler tracks lifetimes as regions of code. When you return a reference, the compiler creates a constraint. The lifetime of the return value must be less than or equal to the lifetime of the data it points to.
In first_word, the input s has some lifetime 'a. The output has lifetime 'b. The compiler sees that the output points into s. It sets 'b to be a subset of 'a. The caller gets a reference that lives as long as s lives. If the caller tries to use the reference after s is dropped, the compiler rejects the code.
You can write the lifetimes explicitly to see the connection.
fn first_word<'a>(s: &'a str) -> &'a str {
// Explicit lifetime 'a ties the input and output.
// The return value cannot outlive s.
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
The explicit version does the same thing. The lifetime parameter 'a appears in both the input and the output. This tells the compiler that the output reference is valid for at least as long as the input reference. Lifetime elision removes the boilerplate when there's only one input reference. The rule is simple. If there is one input reference, the output lifetime is tied to it.
Trust the lifetime elision rules. They cover 99% of cases. Only write explicit lifetimes when you have multiple input references and the output ties to a specific one.
Realistic example
Returning references is common in struct methods. A method on &self can return references into the struct's fields. The lifetime of the return value is tied to the struct instance.
struct Document {
content: String,
}
impl Document {
/// Returns a reference to the first line of the document.
fn first_line(&self) -> &str {
// self is borrowed. We can return references into self.content.
// The return lifetime is tied to &self.
match self.content.find('\n') {
Some(pos) => &self.content[..pos],
None => &self.content[..],
}
}
}
fn main() {
let doc = Document {
content: String::from("Hello\nWorld"),
};
// line borrows from doc.
let line = doc.first_line();
println!("{}", line);
// doc must stay alive while line is used.
}
The method takes &self. This is a reference to the struct. The return type is &str. The compiler ties the return lifetime to &self. The caller gets a reference that lives as long as the struct lives. If the struct is dropped, the reference becomes invalid.
This pattern allows you to expose parts of a struct without copying data. The caller can read the data without taking ownership. The struct retains ownership and can manage the data internally.
Slices are views, not copies. They point to the original data. If the original data moves, the view breaks. Rust prevents the move.
Pitfalls and compiler errors
Returning references triggers specific errors when the lifetime contract is broken. The compiler catches these errors at compile time. You never get a dangling pointer at runtime.
Returning a reference to a local variable
You cannot return a reference to a local variable. Local variables are dropped when the function returns.
fn bad() -> &str {
let s = String::from("hello");
&s // Error: cannot return reference to local variable `s`
}
The compiler rejects this with E0515. The variable s is created inside the function. It is dropped at the end of the function. The reference would point to freed memory. Rust forbids this.
Fix this by returning the value instead of a reference.
fn good() -> String {
let s = String::from("hello");
s // Returns the owned String.
}
Returning a reference to an owned parameter
You cannot return a reference to a parameter that the function owns. The function drops the parameter at the end.
fn bad(s: String) -> &str {
&s // Error: cannot return reference to parameter `s`
}
The compiler rejects this with E0515. The parameter s is owned by the function. The function drops s when it returns. The reference would point to freed memory.
Fix this by taking a reference as input.
fn good(s: &str) -> &str {
s // Returns the borrowed slice.
}
Returning a reference to moved data
You cannot return a reference to data that has been moved. Moving transfers ownership. The original binding no longer holds the data.
fn bad(s: &str) -> &str {
let x = String::from(s);
&x // Error: cannot return reference to local variable `x`
}
The variable x is a local String. It is dropped at the end of the function. The reference points to x. The compiler rejects this.
Fix this by returning a slice of the input.
fn good(s: &str) -> &str {
s // Returns a slice of the input.
}
The borrow checker tracks the map, not the territory. If the territory vanishes, the map is trash.
Decision matrix
Choosing between returning a reference and returning a value depends on ownership and performance. Use the right tool for the job.
Use &T when the caller provides the data and you need to point back to a part of it. The data must outlive the function call. This avoids copying and keeps the caller in control of the memory. Use T when the function creates new data or takes ownership and wants to transfer it back. The caller gets the value, not a pointer. This is necessary when the data is computed inside the function or when you need to modify the data in place. Use Option<&T> when the data might not exist. Returning None avoids panics and keeps the lifetime rules intact. Use Cow<'_, T> when you sometimes borrow and sometimes own. This is the escape hatch for functions that might modify data or create it fresh. Cow stands for "clone on write." It holds either a borrowed reference or an owned value. The caller can handle both cases uniformly.
Reach for plain references when lifetimes are simple. The owned alternative is rarely worth it for small data. Reach for owned values when the data is created locally. The reference alternative is impossible. Reach for Cow when the function signature must accept both borrowed and owned data. The reference alternative is too restrictive.
Conventions and small details
Community conventions make code easier to read and maintain. Follow these patterns to align with the wider Rust ecosystem.
Functions should take &str, not &String. &str accepts both String and string literals. It's more flexible. If you take &String, callers must pass String values. They can't pass literals. &str is the standard for string parameters.
Methods on &self can return references into the struct. The lifetime is tied to the struct instance. This is the standard way to expose struct fields without copying. Methods on &mut self can return mutable references, but the rules are stricter. You can't return a mutable reference if the function also holds an immutable reference to the same data. The borrow checker enforces exclusive access.
Use &s[..] or s to return the whole input. Both work. s coerces to &str automatically. The explicit slice syntax &s[..] is clearer when you're slicing. It shows you're creating a view.
Convention is the explicit form for clarity. &s[..] signals a slice. s signals a coercion. Pick the one that matches your intent.