Question

Returning Strings and &str from Functions in Rust

Question

How should a Rust function return a newly created string, such as a wallpaper file path obtained from a command?

Consider this simplified code:

pub fn get_wallpaper() -> &str {
    let output = Command::new("gsettings");
    // Code that runs the command is omitted.

    if let Ok(message) = String::from_utf8(output.stdout) {
        return message;
    } else {
        return "";
    }
}

This produces a lifetime-related error because the function attempts to return a &str created from data owned inside the function.

However, this function compiles:

fn main() {
    println!("message: {}", hello_string(""));
}

fn hello_string(_input: &str) -> &str {
    "hello world"
}

Why is returning "hello world" as a &str allowed? When is returning &str from a function safe, and when should a function return an owned String instead?

Short Answer

Rust distinguishes between owned text (String) and borrowed text (&str). Return a String when the function creates or receives text that it owns, such as command output. Return &str only when the returned text is borrowed from data that outlives the call, or when it is a string literal with the special lifetime &'static str.

Concept

String and &str both represent UTF-8 text, but they have different ownership rules.

  • String owns its text. The characters are stored in heap memory, and the String is responsible for freeing that memory.
  • &str is a borrowed view of UTF-8 text owned somewhere else. A &str does not keep the text alive.

When a function creates a String, that String is dropped when the function returns unless ownership is moved to the caller. Therefore, returning a reference into that local String would create a dangling reference, which Rust forbids.

fn broken() -> &str {
    let message = String::from("hello");
    &message
}

The reference &message would point at memory that is freed at the end of broken, so this cannot compile.

The appropriate return type for newly produced text is usually String:

fn make_message() -> String {
    String::from("hello")
}

Returning String moves ownership to the caller. The caller now owns the text, so the text remains valid.

A string literal is different. The text in "hello world" is embedded in the compiled program and exists for the entire duration of the program. Its type is &'static str, where 'static means the referenced data lives as long as the program. That is why a function may safely return it.

Mental Model

Think of a String as a box of text that a function owns. When the function finishes, it normally throws away its local boxes. You must hand the entire box to the caller if the caller needs the text later.

A &str is a note containing the location of text in someone else's box. The note is useful only while that box still exists. Rust checks that the note can never outlive the box.

A string literal such as "hello world" is like text printed on a permanent sign in the program building. It is available for the whole program, so returning a note pointing to that sign is safe.

Take Quiz

Syntax and Examples

Use String for text created by the function:

fn greeting(name: &str) -> String {
    format!("Hello, {name}!")
}

fn main() {
    let message = greeting("Ada");
    println!("{message}");
}

format! creates a new owned String. Returning String transfers that ownership to main.

Return a borrowed &str when it comes from an input reference:

fn first_word(text: &str) -> &str {
    match text.find(' ') {
        Some(index) => &text[..index],
        None => text,
    }
}

fn main() {
    let sentence = String::from("Rust is fast");
    let word = first_word(&sentence);
    println!("{word}");
}

Here, the returned slice points into text, which is borrowed from the caller. Lifetime elision lets Rust infer that the result cannot outlive the input.

Return &'static str for a fixed string literal:

fn status_label() -> &'static str {
    "ready"
}

Writing -> &'static str makes the reason explicit: the returned reference points to program-lifetime data. Rust can also infer this in many simple cases:

fn status_label() -> &str {
    "ready"
}

Step by Step Execution

Consider this command-output function:

use std::process::Command;

fn command_text() -> Result<String, std::io::Error> {
    let output = Command::new("printf")
        .arg("hello")
        .output()?;

    Ok(String::from_utf8_lossy(&output.stdout).into_owned())
}

Execution proceeds as follows:

  1. Command::new("printf") creates a command configuration.
  2. .arg("hello") adds an argument.
  3. .output()? runs the command and stores its captured output in a local Output value.
  4. output.stdout is a Vec<u8> owned by that local Output.
  5. String::from_utf8_lossy(&output.stdout) creates text from those bytes. It may borrow the bytes temporarily.
  6. .into_owned() creates an owned String, even if the previous value was borrowing from output.stdout.
  7. Ok(...) returns the String to the caller. The caller owns it.
  8. The local output is dropped safely. The returned String has its own text storage and does not borrow output.stdout.

In contrast, this idea is invalid:

fn invalid() -> &str {
    let message = String::from("hello");
    message.as_str()
}

message.as_str() borrows message. Since message is dropped when invalid ends, Rust rejects the returned reference.

Real World Use Cases

Returning String is common when a function produces text dynamically:

  • Reading a configuration value from a file.
  • Converting command output into text.
  • Building JSON, URLs, SQL fragments, messages, or log lines.
  • Formatting a user-facing error message.
  • Joining path components or normalizing an identifier.

Returning a borrowed &str is useful when a function selects or slices text that the caller already owns:

fn file_extension(path: &str) -> &str {
    path.rsplit_once('.')
        .map(|(_, extension)| extension)
        .unwrap_or("")
}

This does not allocate a new string. It returns a slice of path or the static empty string literal.

Returning &'static str is useful for a small fixed set of labels, constants, or messages:

fn http_method_name(is_post: bool) -> &'static str {
    if is_post {
        "POST"
    } else {
        "GET"
    }
}
Take Quiz

Real Codebase Usage

In production code, a function that runs an external command should generally return Result<String, E> rather than silently converting every failure into an empty string. An empty wallpaper path may be valid-looking data, while a missing command, command failure, or invalid output is an error that callers may need to handle.

A practical pattern is:

use std::process::Command;

fn get_wallpaper() -> Result<String, Box<dyn std::error::Error>> {
    let output = Command::new("gsettings")
        .args(["get", "org.gnome.desktop.background", "picture-uri"])
        .output()?;

    if !output.status.success() {
        return Err(format!("gsettings exited with {}", output.status).into());
    }

    let text = String::from_utf8(output.stdout)?;
    Ok(text.trim().to_owned())
}

This pattern uses:

  • Result to preserve failures.
  • An early return when the command exits unsuccessfully.
  • String::from_utf8 to validate UTF-8 output.
  • trim().to_owned() to remove a trailing newline and create an owned return value.

Also note that Command::new(...) only builds a command. Calling .output() is what executes it and produces an Output containing stdout.

Common Mistakes

Returning a reference to a local String

// Does not compile.
fn make_text() -> &str {
    let text = String::from("generated text");
    text.as_str()
}

Avoid this by returning String:

fn make_text() -> String {
    String::from("generated text")
}

Assuming every &str comes from a function argument

A function with no input references cannot normally return a general borrowed &str, because Rust has no source lifetime to connect to the result. It can return &'static str, however, because string literals live for the whole program.

fn label() -> &'static str {
    "fixed label"
}

Using "" to hide errors

// Lossy: an error and an actual empty result look identical.
fn value() -> String {
    String::new()
}

Use Result<String, E> when failure matters.

Believing String::from_utf8 borrows its input

String::from_utf8(bytes) takes ownership of a Vec<u8> and, if successful, returns an owned String. It is suitable when you own the bytes.

Adding 'static to force compilation

Do not write -> &'static str unless the data is truly static, such as a string literal or a global static value. 'static does not make a local String live forever.

Comparisons

Return typeOwnershipBest useExample source
StringCaller receives ownershipFunction creates, reads, or transforms textformat!, file contents, command output
&strBorrowed; owner remains elsewhereReturn a slice of caller-provided text&text[..5]
&'static strBorrowed from program-lifetime dataFixed literals and constant labels"unknown"
Result<String, E>Owned text on success, error on failureI/O, commands, parsing, fallible workCommand::output()

String versus &str is not primarily a performance choice. It is an ownership and lifetime choice. Borrow &str when valid text already exists elsewhere; return String when the function must provide newly owned text.

String::new() and "" both represent empty text, but their types differ:

let owned: String = String::new();
let borrowed: &str = "";
Take Quiz

Cheat Sheet

  • String owns UTF-8 text.
  • &str borrows UTF-8 text owned elsewhere.
  • Return String when your function creates the text.
fn build() -> String {
    format!("item-{}", 42)
}
  • Return &str when the result is a slice of an input.
fn keep(text: &str) -> &str {
    text
}
  • Return &'static str for string literals.
fn name() -> &'static str {
    "Rust"
}
  • Never return &str pointing into a local String or local Vec<u8>.
  • Use Result<String, E> for text obtained through fallible work such as files, processes, or network requests.
  • .to_owned() and .to_string() can turn a &str into an owned String.
  • String::from_utf8(vec) consumes a Vec<u8> and returns Result<String, FromUtf8Error>.

FAQ

Why does Rust allow a function to return "hello world" as &str?

A string literal is stored in the compiled program and has the lifetime &'static str. It remains valid for the entire program.

Is returning &str always better because it avoids allocation?

No. A returned &str must point to data that will remain alive. Returning String is the correct and safe design when the function creates the data.

Why does fn hello_string(x: &str) -> &str compile?

Rust can infer that the output may be tied to the input lifetime. In the specific implementation, the function instead returns a string literal, which is valid for any shorter lifetime because it is 'static.

Should I write -> &'static str for every literal-returning function?

It is often clearer to do so when the function intentionally returns a fixed literal. Rust may infer it, but the explicit type documents the contract.

Can I convert a local String to &'static str?

Not safely through ordinary borrowing. A local String is dropped when its scope ends. Leaking memory can produce a static reference, but that is almost never the right solution.

How should I return command output in Rust?

Usually return Result<String, E>. Run the command with .output(), check its exit status if needed, decode stdout, and return the owned String.

When should I use Cow<'_, str>?

Use it when an API may sometimes borrow text and sometimes need to allocate owned text. For a first API, String is usually simpler when output is newly produced.

Take Quiz

Mini Project

Description

Build a small helper that executes a command and returns its standard output as cleaned-up text. The project demonstrates why command output should be returned as an owned String and why command failures should be represented with Result.

Goal

Create a function that runs a command, removes surrounding whitespace from its output, and reports errors to the caller.

Requirements

Run the printf command with one text argument. Return the command output as Result<String, Box<dyn std::error::Error>>. Return an error if the command exits unsuccessfully. Remove leading and trailing whitespace from successful output. Print the returned text in main.

Take Quiz

Keep learning

Related questions

View more