yjs/yjs · error · Error

Exceeded content range

Error message

Exceeded content range

What it means

YType.formatText (invoked via applyDelta with retain/format attributes) walks the text while consuming the retained length. If, after consuming the whole content, remaining format length is still > 0, the delta tried to format more characters than the type contains.

Source

Thrown at src/ytype.js:217

              if (length < 0 || (length === 0 && i !== contents.length)) {
                const c = contents[--i]
                getItemCleanStart(transaction, createID(item.id.client, c.clock + c.content.getLength() + length))
              }
            } else {
              // plain content: split directly at the offset
              getItemCleanStart(transaction, createID(item.id.client, item.id.clock + length))
              length = 0
            }
          } else {
            length -= rightLen
          }
          break
        }
      }
      this.forward()
    }
    if (length > 0) {
      throw new Error('Exceeded content range')
    }
    insertNegatedFormats(transaction, parent, this, negatedFormats)
  }
}

/**
 * Negate applied formats
 *
 * @param {Transaction} transaction
 * @param {YType} parent
 * @param {ItemTextListPosition} currPos
 * @param {Map<string,any>} negatedFormats
 *
 * @private
 * @function
 */
const insertNegatedFormats = (transaction, parent, currPos, negatedFormats) => {
  // check if we really need to remove formats

View on GitHub (pinned to 567af9b41f)

Solutions

  1. Check ytext.length (or the delta target's length) before applying and clamp retain so retain + insert <= length.
  2. Re-fetch a fresh delta/snapshot after remote changes instead of reusing a stale one.
  3. Catch the error and fall back to computing a new delta against current content.

Example fix

// before
ytext.applyDelta([{ retain: 100, attributes: { bold: true } }]) // throws if shorter

// after
if (100 <= ytext.length) ytext.applyDelta([{ retain: 100, attributes: { bold: true } }])
else ytext.applyDelta([{ retain: ytext.length, attributes: { bold: true } }])
Defensive patterns

Strategy: validation

Validate before calling

const total = delta.reduce((n, op) => n + (op.retain || 0) + (op.insert?.length || op.insert || 0), 0)
if (total > ytext.length) throw new Error('Delta exceeds text length')
ytext.applyDelta(delta)

Type guard

const deltaFits = (delta, len) => delta.reduce((n, op) => n + (typeof op.retain === 'number' ? op.retain : 0) + (typeof op.insert === 'string' ? op.insert.length : (typeof op.insert === 'number' ? 1 : 0)), 0) <= len

Try / catch

try {
  ytext.applyDelta(delta)
} catch (e) {
  if (e.message === 'Exceeded content range') {
    ytext.applyDelta(delta.slice(0, Math.max(0, ytext.length))) // recompute against fresh state
  } else throw e
}

Prevention

When it happens

Trigger: Calling ytext.applyDelta(delta) where a retain value (plus any inserted length) exceeds the text length, e.g. { retain: 100, attributes: { bold: true } } on a shorter text.

Common situations: Porting Quill deltas computed against a stale editor snapshot; concurrent edits shrank the text before the delta was applied; off-by-one retain values in loops that format text in chunks.

Related errors


AI-assisted analysis of yjs/yjs@567af9b41f (2026-09-01). Data as JSON: /api/errors/d66eb5ce48154429. Report an issue: GitHub.