Strings

Reviewed & published by Brayan K

Rust is a systems programming language focused on speed, memory safety, and fearless concurrency — and its text handling is built on UTF-8 with two complementary types: the owned String and the borrowed &str.

Part of the free Rust course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

In this lesson you'll create and grow strings, concatenate and format them, use common methods, and understand why .len() counts bytes.

What You'll Learn in This Lesson

1️⃣ Creating and Growing Strings

There are two string types to know. A &str is a borrowed slice (string literals are &str), while a String is owned and growable. You build a String with String::from or .to_string(), then grow it with push_str and push.

fn main() {
    // Two main string types:
    //   &str   — a borrowed, fixed string slice (literals are &str)
    //   String — an owned, growable string on the heap

    let literal: &str = "hello";          // a string slice
    let owned: String = String::from("hello");  // an owned String
    let also_owned = "world".to_string(); // another way to make a String

    println!("{} {} {}", literal, owned, also_owned);

    // Grow a String with push_str (a slice) and push (a single char).
    let mut greeting = String::from("Hi");
    greeting.push_str(", there");   // append a &str
    greeting.push('!');             // append a char
    println!("{}", greeting);       // -> Hi, there!

    // .len() is the number of BYTES, not characters.
    println!("byte length: {}", greeting.len());   // -> byte length: 10
}

Note that .len() reported 10 bytes. For this ASCII text that equals the character count, but for accented letters or emoji it would not — strings are UTF-8.

2️⃣ Concatenation, Formatting, and Methods

You can join strings with + (which moves the left side and borrows the right), but the more flexible tool is format!, which builds a String without moving anything. Strings also come with a rich set of methods: trim, to_uppercase, contains, replace, and more.

fn main() {
    let a = String::from("foo");
    let b = String::from("bar");

    // Concatenate with + : the left side is moved, the right side is borrowed.
    let combined = a + &b;          // a is moved here; b is borrowed
    println!("{}", combined);       // -> foobar

    // format! builds a String without moving any of its arguments.
    let name = "Ada";
    let msg = format!("Hello, {}! You have {} messages.", name, 3);
    println!("{}", msg);            // -> Hello, Ada! You have 3 messages.

    // Common, handy methods:
    let text = "  Rust Lang  ";
    println!("trimmed: '{}'", text.trim());            // -> 'Rust Lang'
    println!("upper: {}", "rust".to_uppercase());      // -> RUST
    println!("contains 'Lang'? {}", text.contains("Lang")); // -> true
    println!("replaced: {}", "a-b-c".replace("-", "/")); // -> a/b/c

    // Iterate over characters (NOT bytes) safely.
    for c in "abc".chars() {
        print!("[{}]", c);          // -> [a][b][c]
    }
    println!();
}

Because you can't index a string by position, you iterate with .chars() to walk characters safely. This respects UTF-8 boundaries automatically.

Your turn. Fill in the blanks marked ___, then run it.

// 🎯 YOUR TURN — finish the string handling, then run it.
fn main() {
    // 1) Start with an owned String.
    let mut sentence = String::from("rust");

    // 2) Append " is great" to it (a string slice).
    sentence.___(" is great");       // 👉 the method that appends a &str
    println!("{}", sentence);        // expected: rust is great

    // 3) Build a greeting with format! (no moving of name).
    let name = "Sam";
    let greeting = ___!("Hi, {}!", name);   // 👉 the macro that builds a String
    println!("{}", greeting);        // expected: Hi, Sam!

    // 4) Print the uppercase version of "go".
    println!("{}", "go".to_uppercase());    // expected: GO
}

// ✅ Expected output:
//    rust is great
//    Hi, Sam!
//    GO

Mini-Challenge: A Word Counter

Count words and characters and uppercase a sentence. Run it with cargo run.

// 🎯 MINI-CHALLENGE: a word counter.
//
// 1. Take the string "the quick brown fox" (a &str).
// 2. Use .split_whitespace() to break it into words; .count() counts them.
//    (split_whitespace returns an iterator; .count() consumes it.)
// 3. Print "Words: 4".
// 4. Use .to_uppercase() to print the whole sentence in capitals.
// 5. Use .chars().count() to print the number of characters (no spaces? keep
//    it simple — just count all chars including spaces).
//
// ✅ Example output:
//    Words: 4
//    THE QUICK BROWN FOX
//    Chars: 19

fn main() {
    // your code here
}

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — Strings

GoalRust Syntax
Make a StringString::from("hi")
Append slices.push_str("!")
Append chars.push('!')
Build with valuesformat!("{} {}", a, b)
Iterate charsfor c in s.chars() { }
Byte lengths.len()

🎉 Lesson Complete!

Practice quiz

What is the difference between String and &str?

  • No difference
  • &str is owned; String is borrowed
  • String is owned and growable; &str is a borrowed slice
  • String is on the stack

Answer: String is owned and growable; &str is a borrowed slice. String owns growable heap data; &str is a borrowed, fixed view.

What type is a string literal like "hello"?

  • &str
  • String
  • char
  • Vec<u8>

Answer: &str. String literals have type &str (specifically &'static str).

Which method appends a &str to a String?

  • push
  • add_str
  • concat
  • push_str

Answer: push_str. push_str appends a string slice; push appends a single char.

What does String::len() return?

  • The number of characters
  • The number of bytes
  • The capacity
  • The number of words

Answer: The number of bytes. len() returns the byte length, since strings are UTF-8.

Why can't you index a string with s[0]?

  • UTF-8 bytes may not align to characters
  • It is too slow
  • Strings are immutable
  • Indexing is unsafe

Answer: UTF-8 bytes may not align to characters. A byte index could land mid-character in UTF-8, so direct indexing is disallowed.

What does "foo" + &String::from("bar") via let c = a + &b produce?

  • foo bar
  • barfoo
  • foobar
  • an error

Answer: foobar. + concatenates, moving the left String and borrowing the right, giving foobar.

Which is the cleanest way to build a String from several pieces without moving them?

  • The + operator
  • format!
  • push
  • to_string

Answer: format!. format! borrows its arguments and returns a new String.

How do you iterate over characters safely?

  • .bytes()
  • .len()
  • [i]
  • .chars()

Answer: .chars(). .chars() walks Unicode characters respecting UTF-8 boundaries.

What does "rust".to_uppercase() return?

  • rust
  • RUST
  • Rust
  • an error

Answer: RUST. to_uppercase returns RUST.

What does "the quick brown fox".split_whitespace().count() return?

  • 3
  • 19
  • 4
  • 1

Answer: 4. There are 4 whitespace-separated words.

Continue this course

Frequently asked questions

What is the difference between String and &str?

String is an owned, growable string stored on the heap — you can push onto it and it owns its data. &str (a string slice) is a borrowed, fixed-length view into string data; it does not own anything. String literals like "hello" are &str. A common pattern is to take &str as function parameters for flexibility and to use String when you need to build or own text. A String can be borrowed as &str automatically, but going the other way (turning a &str into a String) requires an explicit conversion like .to_string().

Why does String::len return bytes instead of characters?

Rust strings are UTF-8 encoded, and in UTF-8 a single character can take anywhere from one to four bytes. The .len() method reports the number of bytes the string occupies, which for plain ASCII text equals the character count but for accented letters or emoji does not. If you want the number of Unicode characters, use .chars().count(). This byte-versus-character distinction is also why you cannot index a string with [i] in Rust.

Why can't I index a string like s[0]?

Because strings are UTF-8, byte index 0 might be only part of a multi-byte character, so returning a single byte could give a meaningless fragment. To prevent silent bugs, Rust disallows direct indexing of strings. Instead, you iterate characters with .chars(), iterate bytes with .bytes(), or take a slice with a byte range like &s[0..4] (which must fall on character boundaries or it will panic).

How do I build a string from several pieces?

The cleanest way is the format! macro, which works like println! but returns a String instead of printing: let s = format!("{} {}", a, b). It borrows its arguments, so nothing gets moved. You can also use the + operator (which moves the left operand and borrows the right), push_str to append a slice, or push to append a single char. For joining many pieces, building with format! or pushing in a loop are both common.