A returned reference needs storage that remains valid after the function returns. This function creates a local string and tries to return a slice of its buffer:
fn make_label<'a>() -> &'a str {
let label = String::from("ready");
label.as_str()
}
Rust rejects it with E0515: the returned value references the local
variable label. The string would be dropped on return, freeing the buffer
that the slice refers to.
An annotation cannot extend storage lifetime
The function's output lifetime 'a is not tied to an input. Its signature
promises a reference valid for the lifetime selected at a call site,
including a use after the call completes. The local string cannot satisfy
that promise. Writing &'static str changes the promise but does not stop
the local string from being dropped.
This applies to storage that is actually destroyed on return. A local reference can point to longer-lived storage, and an owned value can be moved to the caller instead of being dropped. The issue is the referent's validity, not whether a variable was declared inside a function.
A string literal is a valid source for a static reference:
fn ready_label() -> &'static str {
"ready"
}
For computed text, choose an API that makes its storage available for as long as the caller needs it.
Return an owned result
fn shout(s: &str) -> String {
let mut out = s.to_uppercase();
out.push('!');
out
}
The returned String owns its buffer. Returning it transfers ownership;
the buffer is not freed at the function's closing brace.
This implementation builds new text. Its allocation count is not fixed:
constructing the uppercase result needs storage, and appending ! may
grow that buffer. Returning owned data is also useful when the output bytes
are unchanged but the result must outlive the input.
Borrow when the answer is a view of the input
pub fn first_word(s: &str) -> &str {
s.split_whitespace().next().unwrap_or("")
}
This returns the first word using Unicode whitespace, or an empty slice when there is no word. It allocates no output string. Lifetime elision ties the returned slice to the input borrow, so the caller must retain the borrowed storage for the slice's uses.
Borrowing is useful when that relationship fits the caller. An owned copy may be preferable if it lets the caller release a much larger input buffer.
Check your understanding How can a parser return the two sides of key=value without allocating strings? Show answer Hide answer
pub fn split_pair(line: &str) -> (&str, &str) {
line.split_once('=').unwrap_or((line, ""))
}
Both outputs are slices. The function splits at the first equals sign, preserving any later ones in the value. Without an equals sign, it returns the whole line and an empty value. It does not trim either side. The tuple and its references are returned by value; the text is borrowed.
Use Cow when some results borrow and others own
use std::borrow::Cow;
pub fn normalize(s: &str) -> Cow<'_, str> {
if s.contains(' ') {
Cow::Owned(s.replace(' ', "_"))
} else {
Cow::Borrowed(s)
}
}
This specific normalization replaces ASCII spaces only. With no spaces,
it returns a view of the input. Otherwise, it returns a new string.
Cow makes both cases explicit in one return type. A caller needing an
independent String can call into_owned(), which copies a borrowed value
or moves an already-owned value. The return type still carries the input
lifetime even though an individual owned variant contains no borrow.
Reuse a buffer owned by the caller
pub fn shout_into(s: &str, out: &mut String) {
out.clear();
for ch in s.chars() {
out.extend(ch.to_uppercase());
}
out.push('!');
}
This writes uppercase characters directly into out, including mappings
that expand one character into several, such as ß to SS. It avoids
constructing a temporary uppercase String.
clear() retains the buffer's capacity. Repeated calls can reuse that
storage; a larger result may still require growth. The caller manages the
buffer and each call replaces its contents. By comparison,
out.push_str(&s.to_uppercase()) creates a temporary string even if out
already has enough capacity.
Deliberately retaining an allocation
fn retained_uppercase(s: &str) -> &'static str {
Box::leak(s.to_uppercase().into_boxed_str())
}
This works because Box::leak consumes the box without automatically
deallocating its contents. Here its mutable reference is coerced to a
shared static string slice. The function provides no way to reclaim that
allocation, so calls that retain new buffers accumulate memory.
Deliberate retention can suit data intended to last for the process's
lifetime. It is not an ordinary replacement for returning String.
Box::leak itself can also produce a shorter-lived reference, and its
documentation describes unsafe reclamation when the caller can establish
the necessary ownership conditions. The example above does neither.
Related error: borrowing a temporary
fn borrow_text(s: &str) -> &str { s }
fn make_text() -> String { String::from("hi") }
fn main() {
let r = borrow_text(&make_text());
println!("{r}");
}
This produces E0716. The temporary string is dropped at the end of the
let statement, but the next statement uses a reference into it.
Give the string an owner that stays in scope:
let owned = make_text();
let r = borrow_text(&owned);
println!("{r}");
A direct binding such as let r = &make_text(); is also valid here because
temporary lifetime extension keeps that temporary alive for the enclosing
block. That rule does not apply to the function-call initializer above.
Temporary lifetimes depend on the expression's syntax; these examples use
edition 2024.
For either error, identify the storage behind the reference and when it is dropped. Then choose whether to keep that owner alive, transfer ownership, or return a view of storage that already has a suitable lifetime.