siyuan-note/siyuan · error

parent block not found

Error message

parent block not found: %s

What it means

CheckContainerParent verifies that a parentID used to locate an insert/move destination actually resolves to an existing container block. If no block tree record exists for the ID, it reports 'parent block not found'. Callers (blockInsert, blockAppend, blockPrepend, validateBlockMove, assetCreateHTML) reject the operation, since inserting into a nonexistent parent would corrupt structure.

Solutions

  1. Verify the parent ID exists (query blocks table or GetBlockTree) before inserting/moving
  2. Refresh plugin-side caches of block IDs after sync/delete operations
  3. Use previousID of an existing sibling instead of a stale parentID, or fetch the current document root ID

Example fix

// before
await kernelFetch("/api/block/insertBlock", { parentID: cachedParentID, data }) // parent deleted
// after
const parent = await kernelFetch("/api/block/getBlockInfo", { id: cachedParentID })
if (!parent) { cachedParentID = await getDocumentRootID() }
await kernelFetch("/api/block/insertBlock", { parentID: cachedParentID, data })
Defensive patterns

Strategy: validation

Validate before calling

const parent = await kernelFetch("/api/block/getBlockInfo", { id: parentID });
if (!parent || !parent.id) throw new Error("parent does not exist: " + parentID);

Try / catch

try {
  await insertBlock({ parentID, data });
} catch (e) {
  if (String(e.message).startsWith("parent block not found")) await refreshBlockCache();
  else throw e;
}

Prevention

When it happens

Trigger: blockInsert/blockAppend/blockPrepend or move APIs invoked with parentID that does not exist — typo'd ID, deleted document/block, stale ID cached by a plugin, or an ID from another workspace whose DB lacks the block.

Common situations: Plugin holds IDs across sessions while documents were deleted; sync removed blocks locally; ID from a different workspace; passing a block ID where a document ID was expected.

Understand the failure class

Background: Record Not Found Errors: "not found", RecordNotFound, and "was not found" — what they mean and how to fix them — this error's family across 28 libraries.

Related errors


AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19). Data as JSON: /api/errors/0d95e048051bf6ab. Report an issue: GitHub.

Appendix: source

Thrown at kernel/treenode/blocktree.go:520

// IsContainerType 按主类型缩写判断块是否为容器块(可合法接收子块)。
// 入参 abbrType 对应 BlockTree.Type(如 "d"/"h"/"p"),由 TypeAbbr 写入。
func IsContainerType(abbrType string) bool {
	switch abbrType {
	case "d", "b", "l", "i", "s", "callout", "tab":
		return true
	}
	return false
}

// CheckContainerParent 校验 parentID 指向的块是否允许接收子块。
// 仅在“通过 parentID 定位插入/移动目标”(即不依赖 previousID/nextID)的场景下调用,
// 因为一旦带 previousID/nextID,事务层走的是兄弟级 InsertAfter/InsertBefore,天然合法。
// 返回 nil 表示合法;返回 error 时调用方应拒绝本次操作。
func CheckContainerParent(parentID string) error {
	bt := GetBlockTree(parentID)
	if nil == bt {
		return fmt.Errorf("parent block not found: %s", parentID)
	}
	if IsContainerType(bt.Type) {
		return nil
	}
	if "h" == bt.Type {
		// 标题是叶子块,其“子内容”在数据结构上实为后续兄弟节点(由 HeadingChildren 按层级推算)。
		// 把块挂成标题的 AST 子节点属于非法嵌套,应改用 previousID 定位。
		return fmt.Errorf("heading [%s] is a leaf block and cannot have children; to place a block below this heading, pass previousID=<heading id> or previousID=<last block below the heading> instead of parentID", parentID)
	}
	return fmt.Errorf("block [%s] type %q is a leaf block and cannot have children; use previousID to place the block as its sibling instead", parentID, bt.Type)
}

// CheckListItemNesting 校验 parentID 和 childID 是否形成“列表项直含列表项”的非法嵌套。
// 嵌套列表的正确结构是 ListItem > List > ListItem,列表项不能直接作为另一个列表项的子块。
// 仅在 move 场景调用(源和目标类型均已知)。
func CheckListItemNesting(parentID, childID string) error {
	parentBt := GetBlockTree(parentID)
	childBt := GetBlockTree(childID)

View on GitHub (pinned to 9f775e8a12)