{"record":{"id":"d66eb5ce48154429","repo":"yjs/yjs","slug":"exceeded-content-range","errorCode":null,"errorMessage":"Exceeded content range","messagePattern":"Exceeded content range","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/ytype.js","lineNumber":217,"sourceCode":"              if (length < 0 || (length === 0 && i !== contents.length)) {\n                const c = contents[--i]\n                getItemCleanStart(transaction, createID(item.id.client, c.clock + c.content.getLength() + length))\n              }\n            } else {\n              // plain content: split directly at the offset\n              getItemCleanStart(transaction, createID(item.id.client, item.id.clock + length))\n              length = 0\n            }\n          } else {\n            length -= rightLen\n          }\n          break\n        }\n      }\n      this.forward()\n    }\n    if (length > 0) {\n      throw new Error('Exceeded content range')\n    }\n    insertNegatedFormats(transaction, parent, this, negatedFormats)\n  }\n}\n\n/**\n * Negate applied formats\n *\n * @param {Transaction} transaction\n * @param {YType} parent\n * @param {ItemTextListPosition} currPos\n * @param {Map<string,any>} negatedFormats\n *\n * @private\n * @function\n */\nconst insertNegatedFormats = (transaction, parent, currPos, negatedFormats) => {\n  // check if we really need to remove formats","sourceCodeStart":199,"sourceCodeEnd":235,"githubUrl":"https://github.com/yjs/yjs/blob/567af9b41fe5e1290e0cfe7fcc025a9f98c514a0/src/ytype.js#L199-L235","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Check ytext.length (or the delta target's length) before applying and clamp retain so retain + insert <= length.","Re-fetch a fresh delta/snapshot after remote changes instead of reusing a stale one.","Catch the error and fall back to computing a new delta against current content."],"exampleFix":"// before\nytext.applyDelta([{ retain: 100, attributes: { bold: true } }]) // throws if shorter\n\n// after\nif (100 <= ytext.length) ytext.applyDelta([{ retain: 100, attributes: { bold: true } }])\nelse ytext.applyDelta([{ retain: ytext.length, attributes: { bold: true } }])","handlingStrategy":"validation","validationCode":"const total = delta.reduce((n, op) => n + (op.retain || 0) + (op.insert?.length || op.insert || 0), 0)\nif (total > ytext.length) throw new Error('Delta exceeds text length')\nytext.applyDelta(delta)","typeGuard":"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","tryCatchPattern":"try {\n  ytext.applyDelta(delta)\n} catch (e) {\n  if (e.message === 'Exceeded content range') {\n    ytext.applyDelta(delta.slice(0, Math.max(0, ytext.length))) // recompute against fresh state\n  } else throw e\n}","preventionTips":["Always base deltas on a freshly read ytext.length","Recompute deltas after any remote update (observe events) instead of caching them","Test formatting code against empty and single-character texts to catch off-by-one retains"],"tags":["delta","quill","formatting","range","yjs"],"backgroundTag":"delta-exceeds-content-range","analyzedSha":"567af9b41fe5e1290e0cfe7fcc025a9f98c514a0","analyzedAt":"2026-09-01T05:43:34.752Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}