Skip to content
← All Rust articles

Rust explainer

Returning references safely: E0515

About 5 min to read. Allow extra time to work through examples.

Understand why a reference cannot outlive its storage, then choose an owned result, a borrowed view, Cow or a caller-managed buffer.

This article uses Rust. You can make it your practice mode to browse related content across the site.

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
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.

step 10 of 22

Ownership III: Lifetimes, Explicitly

Up next Structs that hold references