biomejs/biome · error · anyhow::Error

invalid range: start offset ({start:?}) > end offset ({end:?

Error message

invalid range: start offset ({start:?}) > end offset ({end:?}) for {range:?}

What it means

from_proto::text_range converts an incoming LSP Range to a Biome TextRange by converting both endpoints to byte offsets, then asserts start <= end via anyhow::ensure! (from_proto.rs:38-44). The error fires when the client sent a range whose start position is after its end position (by line, then character), which cannot form a valid TextRange.

Source

Thrown at crates/biome_lsp_converters/src/from_proto.rs:40

            };
            line_index.to_utf8(enc, line_col)
        }
    };

    line_index
        .offset(line_col)
        .with_context(|| format!("position {position:?} is out of range"))
}

/// The function is used to convert a LSP range to TextRange.
pub fn text_range(
    line_index: &LineIndex,
    range: Range,
    position_encoding: PositionEncoding,
) -> Result<TextRange> {
    let start = offset(line_index, range.start, position_encoding)?;
    let end = offset(line_index, range.end, position_encoding)?;
    anyhow::ensure!(
        start <= end,
        "invalid range: start offset ({start:?}) > end offset ({end:?}) for {range:?}"
    );
    Ok(TextRange::new(start, end))
}

View on GitHub (pinned to 7529811358)

Solutions

  1. Normalize the range before sending: order the endpoints by (line, character) and swap them if inverted.
  2. When writing a client, pass positions straight from the editor API instead of reconstructing Range objects manually.
  3. On the server side, handle this conversion error and answer the request with an LSP error response rather than letting it propagate.

Example fix

// before: endpoints swapped (start after end)
const range = { start: { line: 10, character: 5 }, end: { line: 2, character: 0 } };

// after: normalize before sending
function posLe(a, b) {
	return a.line < b.line || (a.line === b.line && a.character <= b.character);
}
const range = posLe(startPos, endPos)
	? { start: startPos, end: endPos }
	: { start: endPos, end: startPos };
Defensive patterns

Strategy: validation

Validate before calling

// Normalize every range before sending it to the server
function posLe(a, b) {
	return a.line < b.line || (a.line === b.line && a.character <= b.character);
}

function normalizeRange(range) {
	return posLe(range.start, range.end) ? range : { start: range.end, end: range.start };
}

const params = { textDocument, range: normalizeRange(selection) };

Type guard

function posLe(a, b) {
	return a.line < b.line || (a.line === b.line && a.character <= b.character);
}

function isValidRange(range) {
	return (
		range !== null &&
		typeof range === "object" &&
		posLe(range.start, range.end)
	);
}

Try / catch

try {
	const edits = await connection.sendRequest("textDocument/rangeFormatting", params);
} catch (err) {
	if (String(err?.message ?? err).includes("invalid range: start offset")) {
		return connection.sendRequest("textDocument/rangeFormatting", {
			...params,
			range: normalizeRange(params.range),
		});
	}
	throw err;
}

Prevention

When it happens

Trigger: A client sends range.start greater than range.end - a backwards user selection the editor did not normalize, or client code constructing Range with the endpoints swapped. Hit by every server handler that converts client-provided ranges (formatting ranges, code action ranges, refactor selections).

Common situations: Custom LSP clients or scripts building Position/Range objects by hand; editors that report inverted selections; copy-pasted range literals with start/end reversed.

Related errors


AI-assisted analysis of biomejs/biome@7529811358 (2026-08-16). Data as JSON: /api/errors/0894562e6540ea25. Report an issue: GitHub.