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

  1. Ensure the replacement payload for a list item starts with a proper list item node
  2. If the block should stop being a list item, update the parent list block instead of the item, or delete and re-insert
  3. 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

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


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)