← Home ← Codex ← DEBT ← Engine
Browse by Category
+ added · updated 7d
← Back to glossary

Rust String vs str

Rust Beginner
debt(d3/e2/b3/t5)
d3 Detectability Operational debt — how invisible misuse is to your safety net

Closest to 'default linter catches the common case' (d3), clippy lints `&String` parameters (ptr_arg) and the `&(mut )?String` code_pattern is machine-detectable; but the UTF-8 byte-slicing panic is silent until runtime, so it sits at the milder end of d3.

e2 Effort Remediation debt — work required to fix once spotted

Closest to 'one-line patch or single-call swap' (e1), the quick_fix is swapping a `&String` parameter for `&str` or dropping a needless `.to_string()`; slightly above e1 because returning a borrowed slice from a local sometimes forces a small signature/ownership change, nudging toward e3.

b3 Burden Structural debt — long-term weight of choosing wrong

Closest to 'localised tax' (b3), the choice applies across all Rust contexts (library/cli/web/queue) but each function's owned-vs-borrowed decision is local; a load-bearing API type signature can ripple to callers, keeping it at b3 rather than b1.

t5 Trap Cognitive debt — how counter-intuitive correct behaviour is

Closest to 'notable trap' (t5), the misconception that `String` and `&str` are interchangeable synonyms is the documented gotcha every Rust beginner eventually learns, and byte-index slicing panicking inside a UTF-8 char reinforces the t5 surprise level.

About DEBT scoring →

Also Known As

string vs str String and &str owned vs borrowed strings rust string slice

TL;DR

String is an owned, growable, heap-allocated UTF-8 buffer; str is a borrowed, immutable view usually seen as the reference &str.

Explanation

Rust has two core string types and beginners routinely confuse them. `String` is an owned, heap-allocated, growable buffer of UTF-8 bytes. It owns its data, can be mutated and extended (`push_str`, `push`, `+`), and is freed when it goes out of scope. `str` is the primitive string slice type: an immutable, dynamically-sized sequence of UTF-8 bytes. You almost never handle a bare `str` by value because its size is not known at compile time; instead you work with `&str`, a fat pointer holding an address and a length that borrows into some existing UTF-8 data.

The key relationship is ownership versus borrowing. A `String` owns a heap allocation. A `&str` borrows a window into UTF-8 bytes that may live on the heap (inside a `String`), in the read-only data section of your binary (a string literal like `"hello"` has type `&'static str`), or anywhere else. Because `String` implements `Deref<Target = str>`, you can pass a `&String` almost anywhere a `&str` is expected, and `&my_string[..]` or `my_string.as_str()` produces a `&str` explicitly. This is why idiomatic function signatures take `&str` rather than `&String`: the slice accepts both owned strings and literals, so callers are not forced to allocate.

Choosing between them follows a simple rule. If a function only needs to read string data, accept `&str`. If it needs to own, store, or grow the string, take or return `String`. Returning `&str` from a function requires a lifetime tied to some input, since the borrow must not outlive the data it points into. Converting is cheap in one direction and allocating in the other: `&str` to `String` copies bytes with `.to_string()` or `.to_owned()`; `String` to `&str` is a free borrow.

Understanding this split unlocks Rust's whole string ecosystem: `format!` produces `String`, string literals are `&str`, `Cow<str>` lets you defer the choice, and slicing must always fall on UTF-8 character boundaries or it panics. Getting the types right avoids needless allocations and lifetime headaches while keeping APIs flexible.

Common Misconception

People think `String` and `&str` are interchangeable synonyms or that `str` is just a shorter alias for `String`. In reality `String` owns a growable heap buffer while `&str` is a borrowed, immutable, fixed view into existing UTF-8 bytes.

Why It Matters

Choosing the wrong type forces callers into needless heap allocations or produces functions that cannot accept string literals, and mishandling borrowed slices creates lifetime errors that block compilation.

Common Mistakes

  • Writing function parameters as `&String` instead of `&str`, forcing callers to allocate a String just to pass a literal.
  • Calling `.to_string()` or `.clone()` on data that only needs to be read, adding an allocation the borrow checker did not require.
  • Trying to store a `&str` in a struct without a lifetime annotation, expecting it to keep its data alive like a `String` would.
  • Slicing a string with byte indices that fall inside a multi-byte UTF-8 character, causing a runtime panic.
  • Returning a `&str` that borrows from a local `String`, which is dropped at the end of the function.

Avoid When

  • You genuinely need to own, grow, or mutate the string and store it beyond the current scope, where `String` is the correct choice.
  • A trait or API contract requires an owned `String` return type and borrowing would leak an internal lifetime.
  • You are building a string incrementally with repeated pushes, where an owned `String` buffer is the right tool.

When To Use

  • Accepting string parameters that a function only reads, using `&str` so literals and owned strings both work without allocation.
  • Returning a substring that borrows from an input, tying the returned `&str` to the caller's lifetime.
  • Deferring the owned-versus-borrowed decision with `Cow<str>` when a value is sometimes copied and sometimes referenced.
  • Storing string literals as `&'static str` constants that live for the whole program with no heap allocation.

Code Examples

✗ Vulnerable
// Forces every caller to own a String, even for a literal.
fn greet(name: &String) -> String {
    // Unnecessary clone: we only read `name`.
    let owned = name.clone();
    format!("Hello, {}", owned)
}

// Returns a slice borrowing from a local String that is dropped -
// this does not compile.
fn first_word(input: String) -> &str {
    let trimmed = input.trim();
    trimmed.split(' ').next().unwrap()
}

fn main() {
    // Must allocate a String just to call greet.
    let msg = greet(&"world".to_string());
    println!("{}", msg);
}
✓ Fixed
// Accepts &str, so both String and literals work with no allocation.
fn greet(name: &str) -> String {
    format!("Hello, {}", name)
}

// Borrow tied to the input's lifetime - the caller owns the data.
fn first_word(input: &str) -> &str {
    input.split(' ').next().unwrap_or("")
}

fn main() {
    // Works with a string literal (&'static str).
    println!("{}", greet("world"));

    // Works with an owned String via Deref coercion.
    let owned = String::from("Rustacean");
    println!("{}", greet(&owned));

    // Borrowed slice lives as long as `sentence`.
    let sentence = String::from("the quick brown fox");
    println!("{}", first_word(&sentence));
}

Added 2 Jul 2026
Views 24
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
0 pings T 0 pings W 0 pings T 0 pings F 0 pings S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 0 pings F 0 pings S 0 pings S 0 pings M 0 pings T 0 pings W 2 pings T 3 pings F 2 pings S 0 pings S 3 pings M 0 pings T 1 ping W 0 pings T 0 pings F 0 pings S 0 pings S 0 pings M 3 pings T 0 pings W
No pings yet today
Brave Search 2 Google 1
Google 4 Perplexity 2 Brave Search 2 ChatGPT 1 Amazonbot 1 PetalBot 1 Ahrefs 1 Applebot 1 Meta AI 1
crawler 14
DEV INTEL Tools & Severity
🟢 Low ⚙ Fix effort: Low
⚡ Quick Fix
Take `&str` in parameters when you only read the data, and return or store `String` only when you need ownership; convert with `.to_string()` when an allocation is genuinely required.
📦 Applies To
library cli web queue-worker
🔗 Prerequisites
🔍 Detection Hints
fn\s+\w+\s*\([^)]*:\s*&(mut\s+)?String\b
Auto-detectable: ✓ Yes clippy
🤖 AI Agent
Confidence: High False Positives: Low ✓ Auto-fixable Fix: Low Context: Function


✓ schema.org compliant