wavetermdev/waveterm · error

error deleting tab %s: %w

Error message

error deleting tab %s: %w

What it means

In the recursive path, after finding the workspace DeleteBlock calls DeleteTab to close the now-empty parent tab. Any error from DeleteTab is wrapped as "error deleting tab %s: %w" with the tab id. The block deletion itself already succeeded; only the tab cascade failed.

Source

Thrown at pkg/wcore/block.go:189

		}
	}
	parentBlockCount, err := deleteBlockObj(ctx, blockId)
	if err != nil {
		return fmt.Errorf("error deleting block: %w", err)
	}
	log.Printf("DeleteBlock: parentBlockCount: %d", parentBlockCount)
	parentORef := waveobj.ParseORefNoErr(block.ParentORef)

	if recursive && parentORef.OType == waveobj.OType_Tab && parentBlockCount == 0 {
		// if parent tab has no blocks, delete the tab
		log.Printf("DeleteBlock: parent tab has no blocks, deleting tab %s", parentORef.OID)
		parentWorkspaceId, err := wstore.DBFindWorkspaceForTabId(ctx, parentORef.OID)
		if err != nil {
			return fmt.Errorf("error finding workspace for tab to delete %s: %w", parentORef.OID, err)
		}
		newActiveTabId, err := DeleteTab(ctx, parentWorkspaceId, parentORef.OID, true)
		if err != nil {
			return fmt.Errorf("error deleting tab %s: %w", parentORef.OID, err)
		}
		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 {

View on GitHub (pinned to a4447c1563)

Solutions

  1. Read the wrapped DeleteTab cause for the specific failure.
  2. Re-check the tab's block count; if it is no longer empty, the cascade is unnecessary and can be skipped.
  3. Retry after resolving the store issue; the block is already deleted, only the tab remains.
  4. Handle the failure gracefully — sendBlockCloseEvent is skipped, so emit block-close UI updates manually.

Example fix

// before
err := wcore.DeleteBlock(ctx, blockId, true) // tab cascade failure aborts
// after
if err := wcore.DeleteBlock(ctx, blockId, true); err != nil && strings.Contains(err.Error(), "error deleting tab") {
    // block was deleted; clean up the empty tab separately
    wcore.DeleteTab(ctx, wsId, tabId, false)
}
Defensive patterns

Strategy: try-catch

Validate before calling

tab, _ := wstore.DBGet[*waveobj.Tab](ctx, tabId)
blocks, _ := wstore.DBGetBlocksForTab(ctx, tabId)
cascade := tab != nil && len(blocks) == 1 // only this block remains

Try / catch

err := wcore.DeleteBlock(ctx, blockId, true)
if err != nil && strings.Contains(err.Error(), "error deleting tab") {
    // block already deleted; handle leftover tab separately
    log.Printf("tab cascade failed: %v", err)
    return nil
}

Prevention

When it happens

Trigger: DeleteTab fails during recursive block deletion of the last block in a tab — e.g. tab lookup failure, tab still has other blocks, or the tab delete transaction fails.

Common situations: Race where another client added a block to the tab between the count check and DeleteTab; store/transaction failure; invalid workspace id passed into DeleteTab.

Related errors


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