gastownhall/beads · error · storage.ErrValidation

%w: update title: %w

Error message

%w: update title: %w

What it means

This error wraps types.ValidateIssueTitle failures when an update patch sets Title. ValidateUpdateRequest validates every set field before the backend touches the row, so a bad title cannot half-apply. The double %w produces a chain of storage.ErrValidation and the underlying title error, both matchable via errors.Is.

Source

Thrown at internal/storage/issueops/aggregate.go:82

		}
	}
	return updates
}

// ValidateUpdateRequest checks mutually exclusive guarded-update options and
// the canonical field values every backend must reject identically. Backends
// call it before touching the row so an invalid patch cannot half-apply.
func ValidateUpdateRequest(request publicops.UpdateRequest) error {
	if request.Claim && (request.ExpectedAssignee != nil || request.ExpectedStatus != nil) {
		return fmt.Errorf("%w: claim cannot use expected assignee or status", storage.ErrValidation)
	}
	if request.ForceAssigneeTransfer && (request.Claim || !request.Patch.Assignee.Set || request.ExpectedAssignee != nil) {
		return fmt.Errorf("%w: invalid forced assignee transfer", storage.ErrValidation)
	}
	patch := request.Patch
	if patch.Title.Set {
		if err := types.ValidateIssueTitle(patch.Title.Value); err != nil {
			return fmt.Errorf("%w: update title: %w", storage.ErrValidation, err)
		}
	}
	if patch.Priority.Set {
		if err := types.ValidateIssuePriority(patch.Priority.Value); err != nil {
			return fmt.Errorf("%w: update priority: %w", storage.ErrValidation, err)
		}
	}
	if patch.EstimatedMinutes.Set {
		if err := types.ValidateIssueEstimatedMinutes(patch.EstimatedMinutes.Value); err != nil {
			return fmt.Errorf("%w: update estimated_minutes: %w", storage.ErrValidation, err)
		}
	}
	if patch.Persistence.Set && !patch.Persistence.Value.IsValid() {
		return fmt.Errorf("%w: invalid persistence mode %q", storage.ErrValidation, patch.Persistence.Value)
	}
	return nil
}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Trim and length-check the title before building the patch; skip setting Title entirely when the value is empty
  2. Read the wrapped inner error message to see the exact title rule violated (length vs emptiness)
  3. Add types.ValidateIssueTitle to your input pipeline as a pre-check

Example fix

// before
patch.Title = publicops.SetField[string]{Set: true, Value: strings.TrimSpace(userInput)} // may be empty
// after
title := strings.TrimSpace(userInput)
if title == "" { return errors.New("title required") }
if err := types.ValidateIssueTitle(title); err != nil { return err }
patch.Title = publicops.SetField[string]{Set: true, Value: title}
Defensive patterns

Strategy: validation

Validate before calling

if patch.Title.Set {
  if err := types.ValidateIssueTitle(patch.Title.Value); err != nil {
    return fmt.Errorf("pre-check title: %w", err)
  }
}

Type guard

func titleOK(s string) bool { return types.ValidateIssueTitle(s) == nil }

Try / catch

err := issueops.ExecuteUpdate(ctx, tx, req)
if errors.Is(err, storage.ErrValidation) && strings.Contains(err.Error(), "update title") {
  // show user the wrapped title rule and let them re-enter
}

Prevention

When it happens

Trigger: ExecuteUpdate with Patch.Title.Set=true and a title value that fails types.ValidateIssueTitle — typically an empty/whitespace title or one exceeding the maximum length.

Common situations: User input passed straight from a form/CLI into the patch without trimming or length checks; automated scripts writing programmatically generated titles that end up empty; locale text wider than the column limit.

Related errors


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