siyuan-note/siyuan · error
document cannot be deleted as a block
Error message
document cannot be deleted as a block [%s]
What it means
PerformBlockOperation explicitly rejects deleting the document root node as if it were an ordinary block: when the resolved node is the tree's Root, it returns "document cannot be deleted as a block [%s]". Document removal must go through the document API instead of the block-transaction path.
Solutions
- Filter out document-type blocks (root IDs) before issuing delete block operations.
- Use /api/filetree/removeDoc for document deletion instead of the block operation API.
- Check the block's type from /api/block/getBlockInfo (data.root_id / type 'd') and branch accordingly.
Example fix
// before
rows.forEach(r => deleteBlock(r.id)); // includes type 'd' rows
// after
rows.filter(r => r.type !== 'd').forEach(r => deleteBlock(r.id));
rows.filter(r => r.type === 'd').forEach(r => fetchPost('/api/filetree/removeDoc', { notebook: r.box, path: r.path })); Defensive patterns
Strategy: validation
Validate before calling
async function isDocumentBlock(id) {
const res = await fetchPost('/api/query/sql', { stmt: `SELECT type, root_id, path, box FROM blocks WHERE id='${id.replace(/'/g, "''")}'` });
return res.data[0] && res.data[0].type === 'd';
} Try / catch
try { await deleteBlock(id); } catch (e) { if (String(e.msg).includes('document cannot be deleted as a block')) await fetchPost('/api/filetree/removeDoc', { notebook: box, path }); else throw e; } Prevention
- Exclude root/document rows (type 'd') from block-level delete loops.
- Route document deletions to /api/filetree/removeDoc.
- Check data.root_id vs id: equal means it's a document root.
When it happens
Trigger: Calling PerformBlockOperation with Action="delete" and an ID that is the document (root) block ID — typically the rootID of a .sy file rather than a child block ID.
Common situations: Scripts iterating over /api/query/sql results that include root blocks (type 'd') and deleting every row; code that captured the document ID instead of a paragraph/heading ID from a selection.
Understand the failure class
Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.
Related errors
- block not found [ ]
- unsupported block operation
- attribute [ ] is only supported on regular document roots
- block [ ] is not a document
- block [ ] is not a document that can declare a child…
AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19).
Data as JSON: /api/errors/366ce70a5fd518d2.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/model/block_operation.go:49
switch operation.Action {
case "appendInsert", "insert":
if operation.Action == "appendInsert" || operation.PreviousID == "" && operation.NextID == "" {
if err = treenode.CheckContainerParent(operation.ParentID); err != nil {
return nil, err
}
}
case "delete":
// 外部删除请求必须命中现存节点,编辑器内部仍可使用幂等删除。
tree, loadErr := LoadTreeByBlockID(operation.ID)
if loadErr != nil {
return nil, loadErr
}
node := treenode.GetNodeInTree(tree, operation.ID)
if node == nil {
return nil, fmt.Errorf("block not found [%s]", operation.ID)
}
if node == tree.Root {
return nil, fmt.Errorf("document cannot be deleted as a block [%s]", operation.ID)
}
default:
return nil, fmt.Errorf("unsupported block operation [%s]", operation.Action)
}
tx := &Transaction{DoOperations: []*Operation{operation}}
if err = performTxSyncLocked(tx); err != nil {
return nil, err
}
return []*Transaction{tx}, nil
}
View on GitHub (pinned to 9f775e8a12)