siyuan-note/siyuan · error
list block has no list item
Error message
list block has no list item
What it means
When the old block being updated is a list item and the new payload parses as a whole list (NodeList), resolveBlockUpdateNode tries to unwrap the first list item from the new list to keep the update type-compatible. If that list contains no list item (or the first content block is not a list item), the update cannot proceed and this error is returned.
Solutions
- Ensure the replacement payload for a list item starts with a proper list item node
- If the block should stop being a list item, update the parent list block instead of the item, or delete and re-insert
- Check that DOM serialization preserved the <ul>/<li> (or data-type NodeListItem) structure
Example fix
// before dataType: "dom", data: "<ul>...</ul>" // replacing a list item with an empty/invalid list // after dataType: "dom", data: '<ul><li data-node-id="..." data-type="NodeListItem"><div class="protyle-action">...</div><div data-node-id="..." data-type="NodeParagraph" class="p">...</div></li></ul>'
Defensive patterns
Strategy: validation
Validate before calling
// when updating a list item, the payload must contain a list item as first content block
if (oldNodeType === "NodeListItem" && !/<li[^>]*data-type="NodeListItem"/i.test(payload.data)) {
throw new Error("updating a list item requires NodeListItem content");
} Try / catch
try {
await updateBlock(payload);
} catch (e) {
if (String(e.msg) === "list block has no list item") {
// unwrap or restructure the list payload before retrying
}
} Prevention
- Preserve <li>/NodeListItem structure when round-tripping list DOM
- Update the parent list block when the item type should change
- Prefer delete + insert for structural list changes
When it happens
Trigger: Updating a list-item block (oldNode.Type == NodeListItem) with new data that parses to a NodeList whose first content block is not a NodeListItem — e.g. replacing a list item with a list containing only nested non-item content, or a malformed list structure after BlockDOM round-trip. Covered by TestResolveSuperBlockListItemAfterBlockDOMRoundTrip.
Common situations: API updates replacing a list item's content with data that changed list structure (item converted to a code block inside the list); DOM round-trips that dropped the list-item wrapper; plugins re-authoring list blocks.
Understand the failure class
Background: "is not a compatible type" / "cannot merge" errors: when a value's type doesn't match what the library requires — this error's family across 65 libraries.
Related errors
- block [ ] not found
- block [ ] type is locked: expected , got
- found invalid ID [ ]
- invalid block ID [ ]
- no such file or directory
AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19).
Data as JSON: /api/errors/8743ffdf3bf29bae.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/model/block_update.go:249
updatedNode.Unlink()
root := &ast.Node{Type: ast.NodeDocument}
root.AppendChild(updatedNode)
ret = &parse.Tree{
Root: root,
Context: &parse.Context{ParseOption: luteEngine.ParseOptions},
}
return
}
func resolveBlockUpdateNode(oldNode, root *ast.Node) (updatedNode *ast.Node, err error) {
updatedNode = firstContentBlock(root)
if nil == updatedNode {
return nil, errors.New("parse tree failed")
}
if ast.NodeListItem == oldNode.Type && ast.NodeList == updatedNode.Type {
listItem := firstContentBlock(updatedNode)
if nil == listItem || ast.NodeListItem != listItem.Type {
return nil, errors.New("list block has no list item")
}
updatedNode = listItem
}
return
}
// pinDescendantBlockIDs 把旧块子树中对应位置的子块 ID 钉回新块,避免更新容器块时 Lute 重新生成
// 子块 ID,导致指向子块的块引用、反链、闪卡等失效。
// 匹配规则:按同级内容块顺序对齐,类型一致才沿用旧 ID;类型不一致时向后查找同类型的旧子块重新
// 对齐,这样插入或删除子块后其余子块仍能匹配上旧 ID,新增的子块保持新生成的 ID。
func pinDescendantBlockIDs(oldNode, updatedNode *ast.Node) {
oldChildren := blockChildrenOf(oldNode)
oldIndex := 0
for _, newChild := range blockChildrenOf(updatedNode) {
if oldIndex >= len(oldChildren) {
break
}
if oldChildren[oldIndex].Type != newChild.Type {View on GitHub (pinned to 9f775e8a12)