{"record":{"id":"deb009d706c416e0","repo":"wavetermdev/waveterm","slug":"block-has-subblocks-must-delete-subblocks-first","errorCode":null,"errorMessage":"block has subblocks, must delete subblocks first","messagePattern":"block has subblocks, must delete subblocks first","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/wcore/block.go","lineNumber":208,"sourceCode":"\t\t}\n\t\tSendActiveTabUpdate(ctx, parentWorkspaceId, newActiveTabId)\n\t}\n\tsendBlockCloseEvent(blockId)\n\treturn nil\n}\n\n// returns the updated block count for the parent object\nfunc deleteBlockObj(ctx context.Context, blockId string) (int, error) {\n\treturn wstore.WithTxRtn(ctx, func(tx *wstore.TxWrap) (int, error) {\n\t\tblock, err := wstore.DBGet[*waveobj.Block](tx.Context(), blockId)\n\t\tif err != nil {\n\t\t\treturn -1, fmt.Errorf(\"error getting block: %w\", err)\n\t\t}\n\t\tif block == nil {\n\t\t\treturn -1, fmt.Errorf(\"block not found: %q\", blockId)\n\t\t}\n\t\tif len(block.SubBlockIds) > 0 {\n\t\t\treturn -1, fmt.Errorf(\"block has subblocks, must delete subblocks first\")\n\t\t}\n\t\tparentORef := waveobj.ParseORefNoErr(block.ParentORef)\n\t\tparentBlockCount := -1\n\t\tif parentORef != nil {\n\t\t\tif parentORef.OType == waveobj.OType_Tab {\n\t\t\t\ttab, _ := wstore.DBGet[*waveobj.Tab](tx.Context(), parentORef.OID)\n\t\t\t\tif tab != nil {\n\t\t\t\t\ttab.BlockIds = utilfn.RemoveElemFromSlice(tab.BlockIds, blockId)\n\t\t\t\t\twstore.DBUpdate(tx.Context(), tab)\n\t\t\t\t\tparentBlockCount = len(tab.BlockIds)\n\t\t\t\t}\n\t\t\t} else if parentORef.OType == waveobj.OType_Block {\n\t\t\t\tparentBlock, _ := wstore.DBGet[*waveobj.Block](tx.Context(), parentORef.OID)\n\t\t\t\tif parentBlock != nil {\n\t\t\t\t\tparentBlock.SubBlockIds = utilfn.RemoveElemFromSlice(parentBlock.SubBlockIds, blockId)\n\t\t\t\t\twstore.DBUpdate(tx.Context(), parentBlock)\n\t\t\t\t\tparentBlockCount = len(parentBlock.SubBlockIds)\n\t\t\t\t}","sourceCodeStart":190,"sourceCodeEnd":226,"githubUrl":"https://github.com/wavetermdev/waveterm/blob/a4447c1563b2df285ab89e76c82f91e1a1a49c1e/pkg/wcore/block.go#L190-L226","documentation":"deleteBlockObj refuses to delete a block whose SubBlockIds list is non-empty, returning \"block has subblocks, must delete subblocks first\". This invariant guarantees no orphaned subblocks; children must be removed before the parent. Callers satisfy it by passing recursive=true to DeleteBlock, which clears subblocks first.","triggerScenarios":"Calling DeleteBlock with recursive=false (or invoking deleteBlockObj directly) on a block that still has subblocks registered in SubBlockIds.","commonSituations":"Non-recursive delete of a parent/iframe block that contains subblocks; stale state where subblocks were added but the parent delete used the wrong flag; a prior failed recursive delete that cleared only some children.","solutions":["Call DeleteBlock with recursive=true so subblocks are deleted first.","Delete each subblock id manually before deleting the parent.","If SubBlockIds are stale (children already gone), retry with recursive=true — nil children are skipped by DeleteBlock.","Check SubBlockIds length before issuing a non-recursive delete."],"exampleFix":"// before\nwcore.DeleteBlock(ctx, parentId, false) // \"block has subblocks...\"\n// after\nwcore.DeleteBlock(ctx, parentId, true) // recursive: subblocks removed first","handlingStrategy":"validation","validationCode":"block, _ := wstore.DBGet[*waveobj.Block](ctx, blockId)\nif block != nil && len(block.SubBlockIds) > 0 {\n    recursive = true // must recurse or delete children first\n}","typeGuard":"func canDeleteDirectly(b *waveobj.Block) bool {\n    return b != nil && len(b.SubBlockIds) == 0\n}","tryCatchPattern":"if err := wcore.DeleteBlock(ctx, blockId, false); err != nil && strings.Contains(err.Error(), \"subblocks\") {\n    return wcore.DeleteBlock(ctx, blockId, true) // retry recursively\n}","preventionTips":["Default to recursive=true unless you explicitly manage subblock lifecycle.","Inspect SubBlockIds before issuing deletes.","After a failed recursive delete, retry it to clear remaining children."],"tags":["wave-terminal","block-deletion","subblocks","invariant-violation"],"backgroundTag":"cascade-delete-failed","analyzedSha":"a4447c1563b2df285ab89e76c82f91e1a1a49c1e","analyzedAt":"2026-09-01T15:26:23.972Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}