Start learning free

LESSON 64 OF 414 · STRINGS I

Exceeding String Bounds in Rust: Panics, Empty Slices and None

Lesson Read the full explanation

When accessing string characters with indices, the most common problem we can run into is exceeding the string's bounds.

Remember, the characters of a string are normally indexed from 0 to length - 1.

Any index which is strictly greater than length - 1 is invalid.

When you try to access a character with an invalid index, Rust panics at runtime:

let car = "ford";

// Valid indices: 0, 1, 2, 3
println!("{}", car.chars().nth(0).unwrap()); // Output: f
println!("{}", car.chars().nth(3).unwrap()); // Output: d

// Invalid indices: 4, 30
println!("{}", car.chars().nth(4).unwrap()); // panics: called `Option::unwrap()` on a `None` value
println!("{}", car.chars().nth(30).unwrap()); // panics: called `Option::unwrap()` on a `None` value

Rust panics when you access a character that doesn't exist, which is actually a good thing. Being told about a mistake early on is much better than silently getting wrong data and running into problems later.

In conclusion, we always want to double check our code to make sure that we don't exceed one string's bounds in our programs.


Assignment
Follow the Coding Tutorial and let's practice with string bounds!


Hint
Look at the examples above if you get stuck.


STEP 1 OF 1
    Ctrl + Enter to check

    RUST · STRINGS I

    Exceeding String Bounds in Rust, explained

    Valid positions

    The characters of a string sit at positions 0 to length - 1. The string "ford" has four, at 0, 1, 2 and 3. Position 4 is one past the end, and it is not valid. The most common slip with strings is asking for a position that does not exist, the "exceeding the bounds" error.

    What happens next depends on how you ask for the character. Rust has three behaviours, and knowing which one you are getting is the point of this lesson.

    1. The call returns None

    chars().nth(i) returns an Option<char>. For a position inside the string it is Some(character), and for a position outside it is None. Nothing goes wrong yet:

    let car = "ford";
    println!("{}", car.chars().nth(3).unwrap());
    println!("{:?}", car.chars().nth(4));
    
    d
    None
    

    The panic comes from unwrap(), which stops the program when it is called on None. This line:

    println!("{}", car.chars().nth(4).unwrap());
    

    ends with the message called `Option::unwrap()` on a `None` value. The same applies to get(4..5) on the string, which returns None. Keeping the Option and handling it with match, if let or unwrap_or avoids the panic completely.

    2. The program panics

    Some operations don't return an Option. Slicing with a range, and indexing a Vec, check the bounds and panic if they are broken, with a message that names the problem:

    let car = "ford";
    let i = 5;
    println!("{}", &car[i..]);
    
    byte index 5 is out of bounds of `ford`
    

    With a Vec<char> named v of length 4, v[i] for i = 4 panics with index out of bounds: the len is 4 but the index is 4. A panic ends the program immediately, instead of letting it continue with wrong data. That is a good thing: a loud failure at the mistake is much easier to debug than a wrong answer found later.

    3. The result is quietly empty

    The trap is a range that starts exactly at the length. A start equal to the length is allowed, because the range is empty, not outside the string:

    let car = "ford";
    println!("[{}]", &car[car.len()..]);
    
    []
    

    There is no panic and no error, only an empty string. Only a start greater than the length panics. This is the bug in the lesson's exercise, and it is harder to spot than a crash, because the program runs to the end and prints something.

    The lesson's exercise

    The code should print the last character of message, which is s. It prints an empty line instead:

    let message = "I love pancakes";
    println!("{}", &message[message.len()..]);
    

    The string has 15 bytes, at positions 0 to 14. The slice starts at position 15, one past the last character, so it is empty. The last character is at position 14, which is message.len() - 1. Moving the start back by one fixes the code:

    let message = "I love pancakes";
    println!("{}", &message[message.len() - 1..]);
    

    This prints s. The slice now starts at the last character and runs to the end.

    Defensive patterns

    • Use the checked accessors when the position comes from input: get(range) returns None instead of panicking, and chars().nth(i) does too.
    • Guard subtractions from the length. The length is unsigned, so on an empty string message.len() - 1 wraps to a huge number in an optimized build and panics in a debug build, and the slice after it panics too. Test is_empty() first.
    • Remember the boundary. A single-character slice &s[i..i + 1] needs i + 1 <= s.len(), which means i must be less than the length.
    • Beware of multi-byte characters. In text that is not ASCII, a byte position can fall in the middle of a character, which also panics.

    Key takeaways

    • Valid positions run from 0 to len() - 1.
    • nth and get return an Option; unwrap() turns a None into a panic.
    • Range slices and Vec indexing panic when the bounds are broken.
    • A slice that starts at len() is empty, not an error, so check that it starts at the right position.
    Keep going with RustAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free