Strings & Text

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Rust 1.99.0 and the 2024 edition, checked on 2026-10-06. Rust has one main owned string type, String, and one main borrowed type, &str; both always hold valid UTF-8. Several more types exist for text that is not guaranteed to be UTF-8 — operating-system strings, file paths, C strings — and Rust keeps them apart so that encoding problems surface at the boundaries instead of deep inside a program.

String and &str

String &str

Ownership

owns a heap buffer

borrows bytes owned elsewhere (a String, the binary, a buffer)

Size

3 words: pointer, capacity, length

2 words: pointer, length

Mutable

yes, can grow (push_str, insert, clear)

no

Created by

String::new(), String::from(".."), "..".to_string(), format!

string literals, &s, &s[..], s.as_str()

Use as parameter

only when the function needs ownership

default: accepts &String too, via deref coercion

fn shout(text: &str) -> String {           // borrow in, own out
    let mut loud = text.to_uppercase();
    loud.push('!');
    loud
}

fn main() {
    let literal: &'static str = "dune";     // stored in the binary
    let owned: String = String::from("emma");
    println!("{} {}", shout(literal), shout(&owned)); // &String coerces to &str

    let mut title = String::with_capacity(32);
    title.push_str("Middle");
    title += "march";                       // String + &str
    let full = title.clone() + " by " + "George Eliot";
    println!("{title} / {full} / {}", title.len());
}

+ takes ownership of its left operand (fn add(self, s: &str) -> String), which is why title.clone() + ... is needed above to keep title. For anything more than two pieces, format! is clearer.

Bytes, chars and grapheme clusters

A String is a Vec<u8> with a UTF-8 guarantee. Text can be viewed at three levels, and they differ for anything but ASCII:

  • bytes (u8): the UTF-8 encoding; len() counts bytes;

  • chars (char): Unicode scalar values, 1—​4 bytes each in UTF-8;

  • grapheme clusters: what a reader perceives as one character — possibly several chars, such as a letter plus a combining accent, or an emoji with a skin-tone modifier. The standard library does not segment graphemes; use the unicode-segmentation crate.

fn main() {
    let title = "Señor 📚";
    println!("bytes: {}", title.len());              // 11
    println!("chars: {}", title.chars().count());    // 7
    for (i, c) in title.char_indices() {
        print!("{i}:{c} ");                           // 0:S 1:e 2:ñ 4:o 5:r 6:  7:📚
    }
    println!();
    println!("{:?}", "é".bytes().collect::<Vec<u8>>());          // [195, 169] -- precomposed
    println!("{:?}", "e\u{301}".chars().collect::<Vec<char>>()); // ['e', '\u{301}'] -- decomposed
}

Why strings cannot be indexed

s[0] does not compile (error E0277, "the type str cannot be indexed by {integer}`"). A byte index would return a fragment of a character for non-ASCII text, a char index would need an O(n) scan, and neither is what users usually mean. Ask for the level you want instead: `s.as_bytes()[0], s.chars().nth(0), or s.chars().next().

Range indexing is allowed, in bytes, but panics if a bound falls inside a character:

fn main() {
    let title = "Señor";
    println!("{}", &title[0..2]);                    // "Se"
    println!("{:?}", title.get(0..3));               // None -- byte 3 is inside 'ñ'
    println!("{}", title.is_char_boundary(3));       // false
    let cut = title.char_indices().nth(3).map_or(title.len(), |(i, _)| i);
    println!("{}", &title[..cut]);                   // "Señ" -- first three chars, safely
    // &title[0..3] would panic: byte index 3 is not a char boundary
}

Common string APIs

Most methods are defined on str, so they work on String and &str alike. Methods that need to change the string in place (push_str, insert, truncate, retain, drain, make_ascii_lowercase) are on String.

Task Methods

Searching

contains, starts_with, ends_with, find, rfind, matches, match_indices (patterns can be a &str, a char, a slice of chars or a closure)

Splitting

split, rsplit, splitn, split_once, split_whitespace, split_terminator, lines, split_inclusive

Trimming

trim, trim_start, trim_end, trim_matches, strip_prefix, strip_suffix, strip_circumfix (1.98)

Transforming

replace, replacen, to_lowercase, to_uppercase, to_ascii_lowercase, repeat, escape_debug

Inspecting

len, is_empty, chars, char_indices, bytes, is_ascii, eq_ignore_ascii_case

Building

push, push_str, insert, insert_str, extend, concat and join on slices of strings

fn main() {
    let line = "  978-0-441-01359-3 ; Dune ; Frank Herbert  ";
    let fields: Vec<&str> = line.split(';').map(str::trim).collect();
    println!("{fields:?}");                                   // ["978-0-441-01359-3", "Dune", "Frank Herbert"]

    let isbn = fields[0].replace('-', "");
    println!("{isbn} {}", isbn.starts_with("978"));
    println!("{:?}", fields[2].split_once(' '));              // Some(("Frank", "Herbert"))
    println!("{:?}", fields[1].find('u'));                    // Some(1)

    let tag = "[sci-fi]";
    println!("{:?}", tag.strip_circumfix('[', ']'));          // Some("sci-fi") -- 1.98
    println!("{:?}", "v2.1".strip_prefix('v'));               // Some("2.1")

    let mut slug = String::from("The Left Hand of Darkness");
    slug.make_ascii_lowercase();
    slug.retain(|c| c.is_ascii_alphanumeric() || c == ' ');
    let slug = slug.split_whitespace().collect::<Vec<_>>().join("-");
    println!("{slug}");                                       // the-left-hand-of-darkness
}

Formatting: format! and the formatting mini-language

format!, print!, println!, eprintln!, write! and panic! share one syntax, checked at compile time. Values can be passed as arguments or captured directly from the surrounding scope by name (since 1.58):

fn main() {
    let title = "Dune";
    let year = 1965;
    let price = 9.5;
    let rating = 4.25_f64;

    println!("{title} ({year})");                 // inline capture
    println!("{0} -- {0}, {1}", title, year);     // positional
    println!("{t}", t = title.to_uppercase());    // named argument (expressions need this form)
    println!("[{:>10}] [{:<6}] [{:^8}]", title, year, "*");  // right, left, centre in a width
    println!("[{:*^12}]", title);                 // custom fill character
    println!("{price:.2} {rating:8.1} {:+}", -3); // precision, width + precision, sign
    println!("{:#x} {:#b} {:o} {:e}", 255, 5, 8, 1500.0); // 0xff 0b101 10 1.5e3
    println!("{:05}", 42);                        // 00042 -- zero padding
    let width = 6;
    println!("[{year:>width$}]");                 // width from a variable
    println!("{:?} {:#?}", Some(title), (1, "a"));// Debug and pretty Debug
    println!("{{literal braces}}");
}

Source: std::fmt

Spec Meaning

{} / {:?} / {:#?}

Display / Debug / pretty Debug

{:x} {:X} {:o} {:b} {:e}

hex, upper hex, octal, binary, scientific (# adds 0x/0b prefixes)

{:5} {:<5} {:^5} {:>5}

minimum width with default, left, centre, right alignment

{:_^9}

fill character before the alignment

{:.3} {:8.3} {:.*}

precision for floats (or max width for strings); .* takes it from an argument

{:+} {:05}

always show the sign; zero-pad to width

{name} {0} {:width$}

named, positional, width from a variable

Your own types become formattable by implementing Display or deriving Debug; see Standard Traits & Operator Overloading. std::fmt::from_fn (1.93) builds a one-off displayable value from a closure without defining a type.

ToString, FromStr and parse

Every type that implements Display gets to_string() through a blanket impl of ToString. The reverse direction is FromStr, which str::parse calls:

use std::fmt;
use std::str::FromStr;

#[derive(Debug, PartialEq)]
struct ShelfCode {
    section: String,
    number: u16,
}

impl FromStr for ShelfCode {
    type Err = String;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let (section, number) = s.split_once('-').ok_or("expected SECTION-NUMBER")?;
        let number = number.parse().map_err(|e| format!("bad number: {e}"))?;
        Ok(ShelfCode { section: section.to_uppercase(), number })
    }
}

impl fmt::Display for ShelfCode {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}-{:03}", self.section, self.number)
    }
}

fn main() {
    let code: ShelfCode = "fic-7".parse().unwrap();
    println!("{code} {}", code.to_string().len());   // FIC-007 7
    println!("{:?}", "fic".parse::<ShelfCode>());     // Err("expected SECTION-NUMBER")
}

Cow<str>: borrow when you can, own when you must

std::borrow::Cow<'a, str> ("clone on write") holds either a borrowed &'a str or an owned String. Functions that usually return their input unchanged but sometimes need to modify it can return a Cow and avoid allocating in the common case:

use std::borrow::Cow;

fn normalise_title(title: &str) -> Cow<'_, str> {
    if title.contains("  ") || title != title.trim() {
        Cow::Owned(title.split_whitespace().collect::<Vec<_>>().join(" "))
    } else {
        Cow::Borrowed(title) // no allocation
    }
}

fn main() {
    for t in ["Dune", "  Dune   Messiah "] {
        let n = normalise_title(t);
        let kind = if matches!(n, Cow::Borrowed(_)) { "borrowed" } else { "owned" };
        println!("{n:?} ({kind})");
    }
    let lossy = String::from_utf8_lossy(&[0x44, 0x75, 0xFF, 0x6E, 0x65]); // Cow<str>
    println!("{lossy}");                                                    // Du�ne
}

Converting bytes to text

  • String::from_utf8(vec) and str::from_utf8(&bytes) validate and return a Result;

  • String::from_utf8_lossy(&bytes) replaces invalid sequences with U+FFFD and returns Cow<str>;

  • String::from_utf8_lossy_owned(vec) (1.99) does the same but consumes a Vec<u8>, reusing its allocation when the bytes are already valid;

  • from_utf8_unchecked skips validation and is unsafe.

fn main() {
    let ok = String::from_utf8(vec![0x44, 0x75, 0x6E, 0x65]);
    let bad = String::from_utf8(vec![0x44, 0xFF]);
    println!("{ok:?} {}", bad.is_err());
    let repaired = String::from_utf8_lossy_owned(vec![0x44, 0xFF, 0x65]); // 1.99
    println!("{repaired}");
}

Strings that are not UTF-8

Owned Borrowed Purpose

OsString

OsStr

strings from the operating system (environment variables, command-line arguments, file names): arbitrary bytes on Unix, potentially ill-formed UTF-16 on Windows

PathBuf

Path

file-system paths; wrappers around OsString/OsStr with path operations (join, extension, file_name, parent)

CString

CStr

NUL-terminated strings for C APIs; no interior NUL bytes

Vec<u8>

[u8]

arbitrary bytes; byte-string literals b"..." produce &[u8; N]

use std::ffi::{CStr, CString, OsStr};
use std::path::Path;

fn main() {
    let path = Path::new("/srv/shelf/covers/dune.jpg");
    println!("{:?} {:?}", path.file_stem(), path.extension().and_then(OsStr::to_str));
    println!("{}", path.display());                  // lossy Display for paths
    let cover = path.with_extension("webp");
    println!("{}", cover.display());

    let c_title: &CStr = c"Dune";                   // C string literal (1.77): &'static CStr
    let owned = CString::new("Emma").expect("no interior NUL");
    println!("{:?} {:?} {}", c_title, owned, c_title.count_bytes()); // "Dune" "Emma" 4
    println!("{}", CString::new("bad\0title").is_err()); // true

    let raw = r#"{"title": "Dune", "path": "C:\shelf"}"#; // raw string: no escapes
    let method: &[u8; 3] = b"GET";
    println!("{raw} {method:?}");
}

Source: std::ffi

Convert at the boundaries: os_str.to_str() returns Option<&str> (fails on non-UTF-8), to_string_lossy() always succeeds with replacement characters, and Path::display() is for printing only. The C types are covered in more depth on FFI & C Interop, and paths on I/O, Files, Processes & Environment.

Common errors

Problem Fix

E0277: the type str cannot be indexed by an integer

use chars().nth(i), as_bytes()[i], or byte ranges on char boundaries

E0308 expected String, found &str (or the reverse)

.to_string()/String::from to own; &s/as_str() to borrow

E0277 "the size for values of type str cannot be known"

use &str or Box<str>, not str by value

panic "byte index N is not a char boundary"

slice at indices from char_indices/find, or use get

E0369 cannot add &str to &str

the left operand must be a String: a.to_string() + b, or format!("{a}{b}")