wavetermdev/waveterm · error
block has subblocks, must delete subblocks first
Error message
block has subblocks, must delete subblocks first
What it means
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.
Source
Thrown at pkg/wcore/block.go:208
}
SendActiveTabUpdate(ctx, parentWorkspaceId, newActiveTabId)
}
sendBlockCloseEvent(blockId)
return nil
}
// returns the updated block count for the parent object
func deleteBlockObj(ctx context.Context, blockId string) (int, error) {
return wstore.WithTxRtn(ctx, func(tx *wstore.TxWrap) (int, error) {
block, err := wstore.DBGet[*waveobj.Block](tx.Context(), blockId)
if err != nil {
return -1, fmt.Errorf("error getting block: %w", err)
}
if block == nil {
return -1, fmt.Errorf("block not found: %q", blockId)
}
if len(block.SubBlockIds) > 0 {
return -1, fmt.Errorf("block has subblocks, must delete subblocks first")
}
parentORef := waveobj.ParseORefNoErr(block.ParentORef)
parentBlockCount := -1
if parentORef != nil {
if parentORef.OType == waveobj.OType_Tab {
tab, _ := wstore.DBGet[*waveobj.Tab](tx.Context(), parentORef.OID)
if tab != nil {
tab.BlockIds = utilfn.RemoveElemFromSlice(tab.BlockIds, blockId)
wstore.DBUpdate(tx.Context(), tab)
parentBlockCount = len(tab.BlockIds)
}
} else if parentORef.OType == waveobj.OType_Block {
parentBlock, _ := wstore.DBGet[*waveobj.Block](tx.Context(), parentORef.OID)
if parentBlock != nil {
parentBlock.SubBlockIds = utilfn.RemoveElemFromSlice(parentBlock.SubBlockIds, blockId)
wstore.DBUpdate(tx.Context(), parentBlock)
parentBlockCount = len(parentBlock.SubBlockIds)
}View on GitHub (pinned to a4447c1563)
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.
Example fix
// before wcore.DeleteBlock(ctx, parentId, false) // "block has subblocks..." // after wcore.DeleteBlock(ctx, parentId, true) // recursive: subblocks removed first
Defensive patterns
Strategy: validation
Validate before calling
block, _ := wstore.DBGet[*waveobj.Block](ctx, blockId)
if block != nil && len(block.SubBlockIds) > 0 {
recursive = true // must recurse or delete children first
} Type guard
func canDeleteDirectly(b *waveobj.Block) bool {
return b != nil && len(b.SubBlockIds) == 0
} Try / catch
if err := wcore.DeleteBlock(ctx, blockId, false); err != nil && strings.Contains(err.Error(), "subblocks") {
return wcore.DeleteBlock(ctx, blockId, true) // retry recursively
} Prevention
- 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.
When it happens
Trigger: Calling DeleteBlock with recursive=false (or invoking deleteBlockObj directly) on a block that still has subblocks registered in SubBlockIds.
Common situations: 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.
Related errors
- error deleting subblock %s: %w
- error getting block: %w
- error deleting block: %w
- block not found: %q
- no WAVETERM_TABID env var set
AI-assisted analysis of wavetermdev/waveterm@a4447c1563 (2026-09-01).
Data as JSON: /api/errors/deb009d706c416e0.
Report an issue: GitHub.