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 = resultView on GitHub (pinned to 28ad3b03b7)
Solutions
- Build operations only via TextOperation builders (retain/insert/remove) or fromJSON — never push raw objects into .ops
- Check for duplicate copies of overleaf-editor-core in node_modules; class identity across two module instances breaks instanceof
- Remove custom op subclasses or extend/patch apply to handle them
- 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
- Never push raw objects or third-party op classes into .ops
- Ensure only one copy of overleaf-editor-core is bundled (check for duplicate module instances)
- Round-trip unknown ops through toJSON/fromJSON to normalize class identity
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
- The operation's base length must be equal to the string's le
- Operation can't retain more chars than are left in the strin
- The operation didn't operate on the whole string.
- resulting string would be too long
- Operation can't retain more chars than are left in the strin
AI-assisted analysis of overleaf/overleaf@28ad3b03b7 (2026-09-03).
Data as JSON: /api/errors/cb18269b8615733f.
Report an issue: GitHub.