overleaf/overleaf · critical · Error
The op traverses more elements than the document has
Error message
The op traverses more elements than the document has
What it means
In transformer(), when consuming skip or delete components, take() must supply document elements for the full length. If the document fragments run out before the component's length is consumed, the op traverses more of the document than exists, so the transform aborts. Like error 222, this signals the op and the document/op it is transformed against are out of sync.
Source
Thrown at services/document-updater/app/js/sharejs/types/text-tp2.js:384
if (chunk.d !== undefined) {
throw new Error(
'The transformed op deletes locally inserted characters - it cannot be purged of the insert.'
)
}
if (typeof chunk === 'number') {
length -= chunk
} else {
append(newOp, chunk)
}
}
}
} else {
// Skip or delete
while (length > 0) {
chunk = take(length, true)
if (chunk === null) {
throw new Error(
'The op traverses more elements than the document has'
)
}
append(newOp, chunk)
if (!chunk.i) {
length -= componentLength(chunk)
}
}
}
}
// Append extras from op1
while ((component = take())) {
if (component.i === undefined) {
throw new Error(`Remaining fragments in the op: ${component}`)
}
append(newOp, component)View on GitHub (pinned to 28ad3b03b7)
Solutions
- Resync from a fresh snapshot and re-apply/transform the op against the correct history order.
- Ensure ops are transformed exactly once, in order, with the correct side ('left'/'right').
- Verify document identity and version before transform (version vector check).
- Deduplicate incoming ops at the update-pipeline level before transforming.
Example fix
// before
updates.forEach(u => op = type.transform(op, u.op, 'right'))
// after
const known = new Set(appliedVersions)
for (const u of updates) {
if (known.has(u.version)) continue // dedupe
op = type.transform(op, u.op, 'right')
} Defensive patterns
Strategy: try-catch
Validate before calling
if (opSkipDeleteLength(op) > docLengthAfter(otherOp)) {
return resyncFromSnapshot(op.docId)
} Type guard
function isTp2Op(op) {
return Array.isArray(op) && op.every(c => typeof c === 'number' && c > 0 || (typeof c === 'object' && (c.i !== undefined) !== (c.d !== undefined)))
} Try / catch
try {
transformed = tp2type.transform(op, otherOp, side)
} catch (e) {
if (e.message === 'The op traverses more elements than the document has') {
await resyncDocument(op.docId) // refetch snapshot + replay history
throw new RetryableError(e)
}
throw e
} Prevention
- Transform each incoming op exactly once; track applied versions.
- Queue ops and apply in strict server order; never transform against a rolled-back doc.
- Deduplicate pubsub-delivered updates by op id/version.
When it happens
Trigger: type.transform(op, otherOp, side) where op's skips/deletes exceed the document length implied by otherOp; transforming against an op from a different document version; applying the same transform twice; side-specific replay where one side already consumed the ops.
Common situations: Race between document-updater's update queue and a snapshot fetch; duplicate ops delivered via Redis pubsub; ops transformed against a doc that was rolled back by a history resync; combining ops from different projects/documents.
Related errors
- must be a string?
- The op traverses more elements than the document has
- Remaining fragments in the op: ${i(component)}
- Operation goes past the end of the document
- The transformed op is invalid
AI-assisted analysis of overleaf/overleaf@28ad3b03b7 (2026-09-03).
Data as JSON: /api/errors/11380da8dd013a8e.
Report an issue: GitHub.