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
- Trim and length-check the title before building the patch; skip setting Title entirely when the value is empty
- Read the wrapped inner error message to see the exact title rule violated (length vs emptiness)
- 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
- Trim and reject empty titles at the input boundary
- Enforce max length in the UI before sending the patch
- Reuse types.ValidateIssueTitle in your own form validation so rules match exactly
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
- %w: update priority: %w
- %w: update estimated_minutes: %w
- no store is open for this workspace
- not found
- db: ChildCounterSQLRepository.NextChildID: parentID must not
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/db8e117b9433e110.
Report an issue: GitHub.