Skip to content
Select theme

How to slice a string safely

Short answer: &text[start..end] cuts a &str at byte offsets, and both offsets have to land on character boundaries. Cut through the middle of a multi-byte character and the program panics. text.get(start..end) returns Option<&str> instead — None where the slice would have panicked.

The previous article in this chapter used char_indices() to find the byte offset of every character; cutting at those offsets is what this page is about. From a fresh project — cargo new slice-demo, then replace src/main.rs with each block below and run it with cargo run. The project name only shows up in cargo’s own output; nothing else here depends on it.

Ada
Lovelace
Love

&text[..3] is the first three bytes, &text[4..] everything from byte 4 to the end, and &text[4..8] a half-open range: the byte at end is not in the slice. The result is a &str that borrows the original — nothing is copied.

The offsets are byte offsets, and that’s the trap. “Ada Lovelace” is all ASCII, so here they double as character positions. The habit built on ASCII text breaks the first time the text isn’t.

fn main() {
let text = "café";
let slice = &text[..4];
println!("{slice}");
}
thread 'main' panicked at src/main.rs:3:22:
end byte index 4 is not a char boundary; it is inside 'é' (bytes 3..5 of string)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

é is two bytes, so in “café” it occupies bytes 3 and 4. Index 4 lands inside it, and a slice has to hold whole characters — the result is still UTF-8 text. The check happens at run time, and the message names the character and its byte range, which is usually enough to spot the mistake. The start end is the same check: &text[4..] panics with start byte index 4 is not a char boundary.

This is also why &text[..1] is not “the first character” — unless the text is ASCII, the first character is more than one byte long.

Some("caf")
None
Some("ca")

get takes the same ranges as the indexing syntax and hands back Option<&str>: Some(slice) when both ends sit on boundaries, None when they don’t. Out-of-range and reversed ranges come back None too. When the offsets come from somewhere else — a file, a user, a search result — get turns a crash into a value you can branch on.

str::is_char_boundary(i) answers “can I cut here?” for a single byte index:

0: []
1: [c]
2: [ca]
3: [caf]
5: [café]

Byte 4 is missing — it’s inside é. Index 0 and len() are always boundaries: the empty slice and the whole string. When the offsets come from char_indices(), as in the previous article, they’re boundaries by construction, so slicing at them can’t panic. That’s the safe way to cut by character count:

[écl]
[éclair]

char_indices().nth(n) is the byte offset where the n-th character starts, so &text[..i] is exactly the first n characters. When the text is shorter, nth returns None and the whole string comes back.