String Indexing in Rust: 5 Fatal Mistakes and How to Avoid Them

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Why won't Rust let you just write s[0]?

\n\n

You're just starting to learn Rust, writing your first "Hello, world!" and suddenly... a compilation error. You try to get the first character of a string, as you've done hundreds of times in Python or JavaScript, and the compiler throws something like the type 'String' cannot be indexed by '{integer}'. Sound familiar?

\n\n

This question is one of the most popular on Stack Overflow. Thousands of developers stumble over this seemingly "quirky" aspect of Rust. But in reality, behind it lies a brilliant and well-thought-out solution that saves your code from hard-to-find bugs. In this article, we'll break down the top mistakes when indexing strings in Rust and, most importantly, learn how to do it correctly.

\n\n

Mistake #1: Attempting direct indexing like in C or Python

\n\n

The very first and most common mistake is trying to write let c = my_string[0];. In Rust, strings are not just arrays of ASCII characters. They are stored in UTF-8 encoding. And this changes everything.

\n\n

Why doesn't this work?

\n

Imagine you have the string "Привет". It has 6 "characters" (from a human perspective). But in computer memory, it takes up 12 bytes (each Cyrillic letter is 2 bytes). If Rust allowed my_string[0], what would it return? The first byte (which by itself means nothing) or the first character? And what if the string contained an emoji that takes up 4 bytes? It would be complete chaos.

\n\n

Rust guarantees safety at the compilation level. Indexing by an integer always works in O(1) and returns a single element. But this is impossible for UTF-8: one "character" (char) can take from 1 to 4 bytes. Therefore, the compiler simply forbids such an operation to prevent you from accidentally getting "garbage" or breaking the encoding.

\n\n
// ❌ Compilation error!\nfn main() {\n    let s = String::from("Привет");\n    // let first = s[0]; // error[E0277]: the type 'String' cannot be indexed by '{integer}'\n    println!("Привет, мир!");\n}
\n\n

Mistake #2: Naive use of slicing without considering character boundaries

\n\n

Yes, slices work in Rust. You can write &s[0..5]. But if you land "inside" a multi-byte character, your program will crash with a panic at runtime. This is called a runtime panic.

\n\n

Example of a fatal error

\n
fn main() {\n    let s = String::from("Здравствуйте"); // 12 characters, but 24 bytes\n    // Let's try to take the first 3 bytes\n    let slice = &s[0..3]; // 'З' takes 2 bytes, 'д' takes 2 bytes.\n    // 3 bytes is half of the second character!\n    println!("{}", slice); // thread 'main' panicked at 'byte index 3 is not a char boundary'\n}
\n\n

This is not just a warning. This is a full program crash. Rust does not forgive unsafe memory handling, even at runtime. If you work with user input (nicknames, comments), you are guaranteed to get such a panic at the first Cyrillic or Asian character.

\n\n

How to avoid: Use the char_indices() method for safe boundary searching or work through chars().

\n\n

Mistake #3: Confusion between bytes, chars, and graphemes

\n\n

This is the most insidious mistake. Even if you learn how to get a char from a string, you might still not get what you expected. Because a char in Rust is a Unicode Scalar Value. But what a human considers a "character" (e.g., "é" or "🇷🇺") can consist of several chars.

\n\n

Breakdown by example

\n
fn main() {\n    let s = String::from("é"); // This could be one char (U+00E9)\n    // or composite: 'e' (U+0065) + '◌́' (U+0301)\n    let composed = String::from("e\\u{0301}"); // the same 'é', but from two chars\n    println!("Length of s: {} bytes, {} chars", s.len(), s.chars().count());\n    println!("Length of comp                

Blogs

Book Recommendations