gastownhall/beads · error
add label %s/%s: %w
Error message
add label %s/%s: %w
What it means
Wraps an error returned by labelRepo.Insert when adding a label to an issue or wisp fails at the storage layer. The message embeds the issue ID and label ("add label %s/%s") with the underlying cause via %w. This fires only after the empty-ID/empty-label guards pass.
Source
Thrown at internal/storage/domain/label.go:67
var _ LabelUseCase = (*labelUseCaseImpl)(nil)
func (u *labelUseCaseImpl) AddLabel(ctx context.Context, issueID, label, actor string) error {
return u.add(ctx, issueID, label, actor, false)
}
func (u *labelUseCaseImpl) AddWispLabel(ctx context.Context, wispID, label, actor string) error {
return u.add(ctx, wispID, label, actor, true)
}
func (u *labelUseCaseImpl) add(ctx context.Context, id, label, actor string, useWisp bool) error {
if id == "" {
return fmt.Errorf("add label: id must not be empty")
}
if label == "" {
return fmt.Errorf("add label: label must not be empty")
}
if err := u.labelRepo.Insert(ctx, id, label, actor, LabelOpts{UseWispsTable: useWisp}); err != nil {
return fmt.Errorf("add label %s/%s: %w", id, label, err)
}
return nil
}
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")View on GitHub (pinned to 71377f2769)
Solutions
- Read the wrapped cause: if it's a not-found/constraint error, verify the issue ID exists (bd show <id>).
- Check for duplicate-label handling — treat 'already exists' errors as success if idempotency is desired.
- Ensure no other process holds the store lock; retry after the conflicting command finishes.
- Retry with a fresh context if cancellation or a transient connection error is reported.
- Use AddLabels (addMany) for batch adds, which tolerates skips more gracefully.
Example fix
// before
err := labelUC.AddLabel(ctx, id, label, actor)
// after: tolerate duplicates explicitly
err := labelUC.AddLabel(ctx, id, label, actor)
if err != nil && strings.Contains(err.Error(), "already exists") {
err = nil // idempotent add
} Defensive patterns
Strategy: try-catch
Validate before calling
// verify the issue exists before inserting a label
if _, err := issueUC.GetIssue(ctx, id); err != nil {
return fmt.Errorf("issue %s not found, cannot add label: %w", id, err)
} Type guard
func isLabelInsertErr(err error) bool {
return err != nil && strings.HasPrefix(err.Error(), "add label ")
} Try / catch
err := uc.AddLabel(ctx, id, label, actor)
if err != nil {
cause := errors.Unwrap(err)
if isAlreadyExists(cause) { return nil } // idempotent
if isNotFound(cause) { return fmt.Errorf("issue %s gone", id) }
return err
} Prevention
- Confirm the target issue exists before labeling
- Design label adds to be idempotent (treat duplicate errors as success)
- Avoid concurrent writers on the same store
- Retry transient storage errors with backoff
When it happens
Trigger: Calling AddLabel/AddWispLabel with valid non-empty arguments where Insert fails: nonexistent issue ID (foreign-key style failure), duplicate label insert, database locked/unavailable, or context cancellation.
Common situations: Typo'd or stale issue IDs referencing deleted issues; adding the same label concurrently from two processes; store file locked by another bd instance; DB connection dropped mid-command.
Related errors
- failed to seed issue_counter for prefix %q at %d: %w
- delete: drop labels: %w
- delete: drop wisp labels: %w
- remove label %s/%s: %w
- set labels: list current: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/5ce98f094a6271f2.
Report an issue: GitHub.