gastownhall/beads · warning · storage.ErrStatusMismatch
%w: %s has status %q, expected %q
Error message
%w: %s has status %q, expected %q
What it means
The status-mismatch branch of the CAS check: the caller declared an expected status but the row currently has a different one. storage.ErrStatusMismatch is returned with actual vs expected so state-machine transitions (e.g. close only if open) can fail safely instead of performing an invalid transition.
Source
Thrown at internal/storage/issueops/update_cas.go:56
isWisp := IsActiveWispInTx(ctx, tx, id)
issueTable, _, _, _ := WispTableRouting(isWisp)
var assignee sql.NullString
var status string
err := tx.QueryRowContext(ctx,
fmt.Sprintf("SELECT assignee, status FROM %s WHERE id = ?", issueTable), id,
).Scan(&assignee, &status)
if errors.Is(err, sql.ErrNoRows) {
return fmt.Errorf("%w: issue %s", storage.ErrNotFound, id)
}
if err != nil {
return fmt.Errorf("failed to read assignee/status for %s: %w", id, err)
}
if expectedAssignee != nil && !actorMatches(assignee.String, *expectedAssignee) {
return fmt.Errorf("%w: %s is held by %q, expected %q", storage.ErrAssigneeMismatch, id, assignee.String, *expectedAssignee)
}
if expectedStatus != nil && status != *expectedStatus {
return fmt.Errorf("%w: %s has status %q, expected %q", storage.ErrStatusMismatch, id, status, *expectedStatus)
}
return nil
}
View on GitHub (pinned to 71377f2769)
Solutions
- Check errors.Is(err, storage.ErrStatusMismatch) and treat the current status (shown in the message) as authoritative
- Re-fetch the issue and skip the transition if it already reached the target state (idempotent handling)
- Retry only after reconciling with the actual status, never in a tight loop
- Use CheckVersionInTx/row-version CAS when the whole row, not just status, must be unchanged
Example fix
// before
closeIssue(id) // fails on second run
// after
err := closeIssue(id)
if errors.Is(err, storage.ErrStatusMismatch) {
iss, _ := store.GetIssue(ctx, id)
if iss.Status == "closed" { return nil } // already closed; idempotent
} Defensive patterns
Strategy: try-catch
Validate before calling
iss, _ := store.GetIssue(ctx, id)
if iss.Status != "open" { return fmt.Errorf("issue %s is %s; refusing transition", id, iss.Status) } Try / catch
err := closeIssueChecked(ctx, tx, id, ver)
if errors.Is(err, storage.ErrStatusMismatch) {
return nil // already in/checked past target status; idempotent no-op
} Prevention
- Make state transitions idempotent: treat already-done as success
- Pre-check status before CAS, but still handle the race
- Use row-version CAS when multiple fields must be unchanged
When it happens
Trigger: ExecuteUpdate with ExpectedFields.Status (commonly 'open' before a close) while the issue is already closed/reopened/in another status — usually because another writer changed it first.
Common situations: Double-close attempts from two agents; retry after a partially-completed close; scripts assuming 'open' on issues already resolved; workflow automation racing human edits.
Related errors
- %w: %s is held by %q, expected %q
- %w: issue %s
- ErrLockHeld
- server: ExternalDoltServer.Start: server already started
- ErrAlreadyClaimed
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/5126dd5f9a837c40.
Report an issue: GitHub.