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

  1. Call DeleteBlock with recursive=true so subblocks are deleted first.
  2. Delete each subblock id manually before deleting the parent.
  3. If SubBlockIds are stale (children already gone), retry with recursive=true — nil children are skipped by DeleteBlock.
  4. 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

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


AI-assisted analysis of wavetermdev/waveterm@a4447c1563 (2026-09-01). Data as JSON: /api/errors/deb009d706c416e0. Report an issue: GitHub.