zed-industries/zed · error
byte index {} is out of bounds of rope (length: {})
Error message
byte index {} is out of bounds of rope (length: {}) What it means
Zed's Rope is the text data structure behind every editor buffer, stored as a sum tree of chunks. assert_char_boundary resolves a byte offset by finding the chunk that contains it; when no chunk matches and the call was made with PANIC=true, the rope aborts with the offending offset and its real length. Nearly every offset-taking rope API funnels through this check, so the panic always means a caller computed an offset against different text than the rope actually contains.
Source
Thrown at crates/rope/src/rope.rs:65
let chunk_offset = offset - start;
item.map(|chunk| chunk.is_char_boundary(chunk_offset))
.unwrap_or(false)
}
#[track_caller]
#[inline(always)]
pub fn assert_char_boundary<const PANIC: bool>(&self, offset: usize) -> bool {
if self.chunks.is_empty() && offset == 0 {
return true;
}
let (start, _, item) = self.chunks.find::<usize, _>((), &offset, Bias::Left);
match item {
Some(chunk) => {
let chunk_offset = offset - start;
chunk.assert_char_boundary::<PANIC>(chunk_offset)
}
None if PANIC => {
panic!(
"byte index {} is out of bounds of rope (length: {})",
offset,
self.len()
);
}
None => {
log::error!(
"byte index {} is out of bounds of rope (length: {})",
offset,
self.len()
);
false
}
}
}
pub fn floor_char_boundary(&self, index: usize) -> usize {
if index >= self.len() {View on GitHub (pinned to f4178619ac)
Solutions
- Clamp the offset before use: offset = offset.min(rope.len()), and snap to a character boundary with rope.clip_offset(offset, Bias::Left) (crates/rope/src/rope.rs:536).
- Recompute offsets from the current buffer snapshot after any await or edit instead of reusing values captured earlier.
- When slicing, clamp both ends of the range to 0..rope.len() before calling range APIs.
- Use the offset and rope length printed in the panic message plus the backtrace (the wrapper, e.g. offset_to_point) to find which call site passed the stale value.
Example fix
// before let point = rope.offset_to_point(offset); // offset captured before an edit // after use sum_tree::Bias; let offset = rope.clip_offset(offset.min(rope.len()), Bias::Left); let point = rope.offset_to_point(offset);
Defensive patterns
Strategy: validation
Validate before calling
use sum_tree::Bias;
fn clamp_to_rope(rope: &Rope, offset: usize) -> usize {
rope.clip_offset(offset.min(rope.len()), Bias::Left)
}
// non-panicking probe of the same check the panic path performs:
fn is_valid_offset(rope: &Rope, offset: usize) -> bool {
rope.assert_char_boundary::<false>(offset)
} Prevention
- Never reuse a numeric offset across an await point; re-derive it from the live snapshot.
- Clamp externally computed offsets (LSP positions, search results from other threads) with clip_offset before passing them in.
- Store snapshots rather than bare offsets when mixing coordinates from different sources.
- Unit-test edge cases: empty rope, offset == len, offset == len + 1.
When it happens
Trigger: Calling offset-based APIs with an offset greater than rope.len(): rope.offset_to_point(offset), rope.offset_to_point_utf16, range slicing, or buffer edits applying a range captured from an older snapshot. The classic producer is an async task that captures an offset, awaits, then applies it after an edit shortened the buffer.
Common situations: Features that snapshot cursor/selection offsets and reuse them after an await raced a buffer edit (format-on-save, lint fixes, collaboration); off-by-one from adding to an offset; using the len() of a substring instead of the rope; tests that build a small rope and index past its end.
Related errors
- byte index {} is out of bounds of `{:?}` (length: {})
- row out of range
- invalid display row {}
- blocking sender returned without value
- anchor's path was never added to multibuffer
AI-assisted analysis of zed-industries/zed@f4178619ac (2026-08-20).
Data as JSON: /api/errors/2bc4e412ff8233b3.
Report an issue: GitHub.