gastownhall/beads · error · storage.ErrValidation
%w: status value of type %T is neither a string nor a types.
Error message
%w: status value of type %T is neither a string nor a types.Status
What it means
CrossesIntoDoneCategoryInTx received a status value that is neither a string nor a types.Status, so it cannot determine the status's reopen category. The error wraps storage.ErrValidation and reports the offending Go type via %T.
Source
Thrown at internal/storage/issueops/update.go:306
// a false. Both write funnels ask this question to decide whether close policy
// applies, so answering "no crossing" for a value nobody can read would let an
// in-process caller that got the transport wrong land status='closed' with the
// policy gate skipped — on an issue with open children, no less. Refusing here
// gives a mis-typed status the same fail-loud handling the mis-typed override
// key already gets (see PopForceClosePolicy).
func CrossesIntoDoneCategoryInTx(ctx context.Context, tx DBTX, oldStatus types.Status, updates map[string]interface{}) (bool, error) {
rawStatus, hasStatus := updates["status"]
if !hasStatus {
return false, nil
}
var newStatus types.Status
switch value := rawStatus.(type) {
case string:
newStatus = types.Status(value)
case types.Status:
newStatus = value
default:
return false, fmt.Errorf("%w: status value of type %T is neither a string nor a types.Status", storage.ErrValidation, rawStatus)
}
newCategory, err := ReopenCategoryInTx(ctx, tx, newStatus)
if err != nil {
return false, err
}
if newCategory != types.CategoryDone {
return false, nil
}
oldCategory, err := ReopenCategoryInTx(ctx, tx, oldStatus)
if err != nil {
return false, err
}
return oldCategory != types.CategoryDone, nil
}
// UpdateResult holds the result of an UpdateIssueInTx call.
type UpdateResult struct {View on GitHub (pinned to 71377f2769)
Solutions
- Cast status to string or types.Status before placing it in the updates map.
- If the value may be absent, only set updates["status"] when it is a valid non-empty string.
- Validate status against types.Status values before calling update.
Example fix
// before
updates["status"] = someInterfaceValue
// after
s, ok := someInterfaceValue.(string)
if !ok {
return fmt.Errorf("bad status type %T", someInterfaceValue)
}
updates["status"] = types.Status(s) Defensive patterns
Strategy: type-guard
Validate before calling
v, ok := updates["status"]
if ok {
if _, isStr := v.(string); !isStr {
if _, isStatus := v.(types.Status); !isStatus {
return fmt.Errorf("status must be string or types.Status, got %T", v)
}
}
} Type guard
func validStatusValue(v interface{}) (types.Status, bool) {
switch s := v.(type) {
case string:
return types.Status(s), true
case types.Status:
return s, true
}
return "", false
} Try / catch
if _, err := storage.UpdateIssue(ctx, id, updates, actor); err != nil {
if errors.Is(err, storage.ErrValidation) && strings.Contains(err.Error(), "neither a string nor a types.Status") {
return fmt.Errorf("bad status in update map: %w", err)
}
return err
} Prevention
- Build update maps with typed literals, not decoded interface{} values.
- Coerce JSON-decoded values (float64/interface{}) to string before storing status.
- Centralize update-map construction in one validated helper.
- Add a unit test that every status key is string or types.Status.
When it happens
Trigger: Passing a non-string updates["status"] into updateIssueInTx — e.g. nil, an int, a fmt.Stringer, *types.Status pointer, or a custom type — from code constructing the update map programmatically.
Common situations: JSON unmarshaling into interface{} producing float64 or nil for status; reflection-driven updaters passing typed enums from other packages; a refactor changing types.Status that leaves pointers or wrapper types in the map.
Related errors
- %w: refusing to clear closed_at on %s: its status stays %q,
- %w: refusing to set closed_at on %s: its status stays %q, an
- 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/2178e486f77e23fb.
Report an issue: GitHub.