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

  1. Read the wrapped cause: if it's a not-found/constraint error, verify the issue ID exists (bd show <id>).
  2. Check for duplicate-label handling — treat 'already exists' errors as success if idempotency is desired.
  3. Ensure no other process holds the store lock; retry after the conflicting command finishes.
  4. Retry with a fresh context if cancellation or a transient connection error is reported.
  5. 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

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


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