siyuan-note/siyuan · error
block [ ] type is locked: expected , got
Error message
block [%s] type is locked: expected %s, got %s
What it means
validateBlockUpdateType enforces the LockType option of the block update API: when lockType is true, the replacement block must keep the same node type as the original (empty paragraph blocks are exempt since they carry no meaningful type). If the type changes, the update is rejected with a message showing the block ID, expected type, and actual type. This prevents an update from silently converting e.g. a paragraph into a heading.
Solutions
- Set lockType=false (or omit it) if changing the block type is intended
- Keep the replacement payload's outermost block type identical to the existing block (e.g. wrap content in a paragraph node when updating a paragraph)
- Use delete + insert operations when the block type must change
- Match the message's expected/got types to adjust the payload accordingly
Example fix
// before
{"id": "...", "dataType": "markdown", "data": "# heading text", "lockType": true} // updating a paragraph
// after
{"id": "...", "dataType": "markdown", "data": "heading text", "lockType": true} Defensive patterns
Strategy: validation
Validate before calling
// only send lockType=true when the outermost block type is unchanged
if (payload.lockType && outerBlockTypeOf(payload.data) !== currentBlockType) {
payload.lockType = false; // or transform the payload
} Type guard
const sameType = (oldT, newT) => oldT === newT || oldT === "NodeParagraph";
Try / catch
try {
await updateBlock(payload);
} catch (e) {
if (String(e.msg).includes("type is locked")) {
// retry with lockType=false or fix the payload's block type
}
} Prevention
- Only enable lockType when the update preserves block type
- Parse the expected/got types from the error message to fix payloads
- Use delete + insert when a type change is required
When it happens
Trigger: Calling buildBlockUpdateOperations / doUpdate (the updateBlock API) with LockType=true while the new payload parses to a different block type than the existing block — e.g. replacing a NodeParagraph with a NodeHeading, or a paragraph with a code block.
Common situations: Clients that pass lockType=true out of habit while sending structurally different content; plugins updating a block's content but accidentally changing its element type; automation converting block types when only text edit was intended.
Understand the failure class
Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.
Related errors
- Field [level] should be of type [Number]
- Field [notebook] should be of type [String]
- Field [preview] should be of type [Boolean]
- Field [ ] should be of type [Array]
- Field [ ] should be of type [ ]
AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19).
Data as JSON: /api/errors/660596d008d6222c.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/model/block_update.go:316
}
func firstContentBlock(parent *ast.Node) *ast.Node {
if nil == parent {
return nil
}
for child := parent.FirstChild; nil != child; child = child.Next {
if child.IsBlock() && ast.NodeKramdownBlockIAL != child.Type {
return child
}
}
return nil
}
func validateBlockUpdateType(oldNode, updatedNode *ast.Node, lockType bool) error {
if !lockType || oldNode.Type == updatedNode.Type || isEmptyParagraphBlock(oldNode) {
return nil
}
return fmt.Errorf("block [%s] type is locked: expected %s, got %s",
oldNode.ID, oldNode.Type.String(), updatedNode.Type.String())
}
func isEmptyParagraphBlock(node *ast.Node) bool {
if nil == node || ast.NodeParagraph != node.Type {
return false
}
for child := node.FirstChild; nil != child; child = child.Next {
switch child.Type {
case ast.NodeText:
text := strings.ReplaceAll(string(child.Tokens), "\u200b", "")
if "" != strings.TrimSpace(text) {
return false
}
case ast.NodeSoftBreak, ast.NodeBr:
default:
return false
}View on GitHub (pinned to 9f775e8a12)