overleaf/overleaf · error · UnprocessableError

Unknown ScanOp type during apply

Error message

Unknown ScanOp type during apply

What it means

TextOperation.apply iterates its internal ScanOp list (RetainOp, InsertOp, RemoveOp) and throws UnprocessableError if it encounters a ScanOp instance it does not recognize. In practice this only happens if the internal op array was constructed with foreign or corrupted objects that bypass the normal builders/fromJSON validation.

Source

Thrown at libraries/overleaf-editor-core/lib/operation/text_operation.js:321

          throw new ApplyError(
            "Operation can't retain more chars than are left in the string.",
            op.toJSON(),
            str
          )
        }
        result += str.slice(inputCursor, inputCursor + op.length)
        inputCursor += op.length
      } else if (op instanceof InsertOp) {
        file.comments.applyInsert(
          new Range(result.length, op.insertion.length),
          { commentIds: op.commentIds }
        )
        result += op.insertion
      } else if (op instanceof RemoveOp) {
        file.comments.applyDelete(new Range(result.length, op.length))
        inputCursor += op.length
      } else {
        throw new UnprocessableError('Unknown ScanOp type during apply')
      }
    }

    if (inputCursor !== str.length) {
      throw new TextOperation.ApplyError(
        "The operation didn't operate on the whole string.",
        operation,
        str
      )
    }

    if (result.length > TextOperation.MAX_STRING_LENGTH) {
      throw new TextOperation.TooLongError(operation, result.length)
    }

    file.trackedChanges.applyTextOperation(this)

    file.content = result

View on GitHub (pinned to 28ad3b03b7)

Solutions

  1. Build operations only via TextOperation builders (retain/insert/remove) or fromJSON — never push raw objects into .ops
  2. Check for duplicate copies of overleaf-editor-core in node_modules; class identity across two module instances breaks instanceof
  3. Remove custom op subclasses or extend/patch apply to handle them
  4. Log the offending element with JSON.stringify to identify its actual constructor

Example fix

// before
op.ops.push({ length: 3 }) // plain object, not a ScanOp
// after
op.retain(3) // proper RetainOp instance
Defensive patterns

Strategy: validation

Validate before calling

function hasOnlyKnownScanOps(op) {
  return op.ops.every(o => o instanceof RetainOp || o instanceof InsertOp || o instanceof RemoveOp)
}
// guard: if (!hasOnlyKnownScanOps(op)) rebuild via fromJSON before apply

Type guard

function isScanOp(o) { return o instanceof RetainOp || o instanceof InsertOp || o instanceof RemoveOp }

Try / catch

try {
  file.apply(op)
} catch (err) {
  if (err.name === 'UnprocessableError' && err.message.includes('Unknown ScanOp type')) {
    logger.warn({ ops: op.ops.map(String) }, 'foreign op instance detected')
    return TextOperation.fromJSON(op.toJSON())
  }
  throw err
}

Prevention

When it happens

Trigger: Manually pushing objects into the ops array (e.g. o.ops.push(new SomeOtherOp()) or plain objects instead of RetainOp/InsertOp/RemoveOp instances), or deserialization code that substitutes different op classes.

Common situations: Custom subclasses of scan ops not registered with apply's instanceof checks; monkey-patched or vendored library versions mixing classes across two copies of overleaf-editor-core (duplicate module instances so instanceof fails); test stubs injecting fake ops.

Related errors


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