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
- Read the wrapped cause; treat 'not found'/'no rows' errors as success if idempotent removal is intended.
- Verify the issue exists (bd show <id>) before removing labels.
- Check for lock contention with other bd processes and retry when idle.
- Retry with a fresh context for transient connection or cancellation errors.
- 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
- Check labels exist (GetLabels) before removing, for idempotency
- Avoid double-remove in scripts by tracking which labels were already processed
- Verify the issue ID exists before label operations
- Retry transient storage errors; treat not-found as success where appropriate
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
- delete: drop labels: %w
- delete: drop deps: %w
- delete: drop wisp deps: %w
- delete: drop wisp labels: %w
- delete: drop events: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/0b82c5cfa6250965.
Report an issue: GitHub.