Why won't Rust let you just write s[0]?
\n\nYou'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?
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\nMistake #1: Attempting direct indexing like in C or Python
\n\nThe 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.
Why doesn't this work?
\nImagine 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.
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\nMistake #2: Naive use of slicing without considering character boundaries
\n\nYes, 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.
Example of a fatal error
\nfn 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\nThis 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\nHow to avoid: Use the char_indices() method for safe boundary searching or work through chars().
Mistake #3: Confusion between bytes, chars, and graphemes
\n\nThis 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.
Breakdown by example
\nfn 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