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

  1. 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).
  2. Recompute offsets from the current buffer snapshot after any await or edit instead of reusing values captured earlier.
  3. When slicing, clamp both ends of the range to 0..rope.len() before calling range APIs.
  4. 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

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


AI-assisted analysis of zed-industries/zed@f4178619ac (2026-08-20). Data as JSON: /api/errors/2bc4e412ff8233b3. Report an issue: GitHub.