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

  1. Resync from a fresh snapshot and re-apply/transform the op against the correct history order.
  2. Ensure ops are transformed exactly once, in order, with the correct side ('left'/'right').
  3. Verify document identity and version before transform (version vector check).
  4. 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

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


AI-assisted analysis of overleaf/overleaf@28ad3b03b7 (2026-09-03). Data as JSON: /api/errors/11380da8dd013a8e. Report an issue: GitHub.