gastownhall/beads · error

remove label %s/%s: %w

Error message

remove label %s/%s: %w

What it means

Wraps an error returned by labelRepo.Delete when removing a label fails at the storage layer. The message embeds the issue ID and label ("remove label %s/%s") with the underlying cause via %w. It fires only after the empty-ID/empty-label guards pass.

Source

Thrown at internal/storage/domain/label.go:88

}

func (u *labelUseCaseImpl) RemoveLabel(ctx context.Context, issueID, label, actor string) error {
	return u.remove(ctx, issueID, label, actor, false)
}

func (u *labelUseCaseImpl) RemoveWispLabel(ctx context.Context, wispID, label, actor string) error {
	return u.remove(ctx, wispID, label, actor, true)
}

func (u *labelUseCaseImpl) remove(ctx context.Context, id, label, actor string, useWisp bool) error {
	if id == "" {
		return fmt.Errorf("remove label: id must not be empty")
	}
	if label == "" {
		return fmt.Errorf("remove label: label must not be empty")
	}
	if err := u.labelRepo.Delete(ctx, id, label, actor, LabelOpts{UseWispsTable: useWisp}); err != nil {
		return fmt.Errorf("remove label %s/%s: %w", id, label, err)
	}
	return nil
}

func (u *labelUseCaseImpl) AddLabels(ctx context.Context, issueID string, labels []string, actor string) error {
	return u.addMany(ctx, issueID, labels, actor, false)
}

func (u *labelUseCaseImpl) AddWispLabels(ctx context.Context, wispID string, labels []string, actor string) error {
	return u.addMany(ctx, wispID, labels, actor, true)
}

func (u *labelUseCaseImpl) addMany(ctx context.Context, id string, labels []string, actor string, useWisp bool) error {
	if id == "" {
		return fmt.Errorf("add labels: id must not be empty")
	}
	opts := LabelOpts{UseWispsTable: useWisp}
	for _, label := range labels {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Read the wrapped cause; treat 'not found'/'no rows' errors as success if idempotent removal is intended.
  2. Verify the issue exists (bd show <id>) before removing labels.
  3. Check for lock contention with other bd processes and retry when idle.
  4. Retry with a fresh context for transient connection or cancellation errors.
  5. Use RemoveLabels (batch form) when removing many labels so per-label failures are easier to handle.

Example fix

// before
err := labelUC.RemoveLabel(ctx, id, label, actor) // fails when label already gone

// after: idempotent remove
err := labelUC.RemoveLabel(ctx, id, label, actor)
if err != nil && isNotFound(err) {
    err = nil // label already absent
}
Defensive patterns

Strategy: try-catch

Validate before calling

// verify the issue exists and fetch current labels before removing
labels, err := uc.GetLabels(ctx, id)
if err != nil { return err }
if !slices.Contains(labels, label) { return nil // already absent — idempotent no-op }

Type guard

func isLabelRemoveErr(err error) bool {
    return err != nil && strings.HasPrefix(err.Error(), "remove label ")
}

Try / catch

err := uc.RemoveLabel(ctx, id, label, actor)
if err != nil {
    cause := errors.Unwrap(err)
    if isNotFound(cause) || isNoRows(cause) { return nil } // idempotent remove
    return err
}

Prevention

When it happens

Trigger: Calling RemoveLabel/RemoveWispLabel with valid arguments where Delete fails: the issue doesn't exist, the label row is absent, database locked/unavailable, or context cancellation.

Common situations: Removing a label that was already deleted (double-remove in a script); typo'd issue ID pointing at a nonexistent issue; concurrent bd processes contending for the store lock; dropped DB connection.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/0b82c5cfa6250965. Report an issue: GitHub.