{"record":{"id":"49789f7704eae302","repo":"siyuan-note/siyuan","slug":"load-block-tree-s-failed-tree-is-empty","errorCode":null,"errorMessage":"load block tree [%s] failed: tree is empty","messagePattern":"load block tree \\[(.+?)\\] failed: tree is empty","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"kernel/model/block_update.go","lineNumber":116,"sourceCode":"\t\t\treturn nil, nil, parseErr\n\t\t}\n\n\t\tvar oldTree *parse.Tree\n\t\tvar cacheKey blockUpdateTreeKey\n\t\thasCacheKey := false\n\t\tif blockTree := resolveTree(input.ID); nil != blockTree {\n\t\t\tcacheKey = blockUpdateTreeKey{boxID: blockTree.BoxID, rootID: blockTree.RootID}\n\t\t\thasCacheKey = true\n\t\t\toldTree = treeCache[cacheKey]\n\t\t}\n\t\tif nil == oldTree {\n\t\t\tvar loadErr error\n\t\t\toldTree, loadErr = loadTree(input.ID)\n\t\t\tif loadErr != nil {\n\t\t\t\treturn nil, nil, fmt.Errorf(\"load block tree [%s] failed: %w\", input.ID, loadErr)\n\t\t\t}\n\t\t\tif nil == oldTree || nil == oldTree.Root {\n\t\t\t\treturn nil, nil, fmt.Errorf(\"load block tree [%s] failed: tree is empty\", input.ID)\n\t\t\t}\n\t\t\ttreeCache[blockUpdateTreeKey{boxID: oldTree.Box, rootID: oldTree.ID}] = oldTree\n\t\t\tif hasCacheKey {\n\t\t\t\ttreeCache[cacheKey] = oldTree\n\t\t\t}\n\t\t}\n\t\toldNode := treenode.GetNodeInTree(oldTree, input.ID)\n\t\tif nil == oldNode {\n\t\t\treturn nil, nil, fmt.Errorf(\"block [%s] not found\", input.ID)\n\t\t}\n\n\t\tif _, ok := rootIDSet[oldTree.ID]; !ok {\n\t\t\trootIDSet[oldTree.ID] = struct{}{}\n\t\t\trootIDs = append(rootIDs, oldTree.ID)\n\t\t}\n\n\t\tif ast.NodeDocument == oldNode.Type {\n\t\t\tif validateErr := treenode.ValidateBlockSubtree(dataTree.Root); validateErr != nil {","sourceCodeStart":98,"sourceCodeEnd":134,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/model/block_update.go#L98-L134","documentation":"After loadTree succeeds, buildBlockUpdateOperations defensively checks that the parsed tree and its root are non-nil; otherwise it returns \"load block tree [%s] failed: tree is empty\". This indicates the document loaded but produced an unusable AST — a corrupt or empty .sy file.","triggerScenarios":"Updating a block inside a .sy file that parses to an empty tree: zero-byte or truncated document file (interrupted write/sync), or corrupted JSON content that parses without error but yields no root.","commonSituations":"Power loss or crash mid-write leaving a truncated .sy file; sync engine materializing an incomplete snapshot; manual edits to data/*.sy breaking the JSON structure.","solutions":["Restore the affected document from SiYuan history (文档历史) or from your sync source.","Inspect the .sy file under data/<notebook>/<path>.sy for truncation/corruption; replace it with a valid copy.","Avoid hand-editing .sy files; regenerate documents through the API instead.","If sync-related, re-sync the notebook from a known-good peer before retrying the update."],"exampleFix":null,"handlingStrategy":"fallback","validationCode":"async function docHealthy(rootId) {\n  const res = await fetchPost('/api/export/preview', { id: rootId }); // or getDoc\n  return res.code === 0 && res.data && res.data.content && res.data.content.length > 0;\n}","typeGuard":null,"tryCatchPattern":"try { await updateBlocks(inputs); } catch (e) { if (String(e.msg).includes('tree is empty')) { console.error('document file corrupt; restore from history/sync before editing'); } throw e; }","preventionTips":["Never hand-edit .sy files under data/.","Ensure clean shutdowns and verify sync snapshots complete before editing.","Keep document history/snapshots enabled so corrupt files are recoverable."],"tags":["corrupt-file","empty-tree","data-loss","kernel"],"backgroundTag":"file-read-failed","analyzedSha":"9f775e8a12daef8255556097396f9b2739078892","analyzedAt":"2026-09-19T03:17:15.984Z","contentChangedAt":"2026-09-19T03:17:15.984Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}