gastownhall/beads · error · storage.ErrAlreadyExists
%w: %s
Error message
%w: %s
What it means
The sentinel duplicate error: the ID was found in the issues or wisps table, so EnsureIssueIDAvailableInTx returns fmt.Errorf("%w: %s", storage.ErrAlreadyExists, id). Callers match it with errors.Is(err, storage.ErrAlreadyExists). It is the guard's normal refusal for occupied IDs, raised after coordination serializes the create.
Source
Thrown at internal/storage/issueops/create_only_guard.go:30
// EnsureIssueIDAvailableInTx serializes same-shard creates and rejects occupied IDs.
func EnsureIssueIDAvailableInTx(ctx context.Context, tx DBTX, id string) error {
if tx == nil {
return fmt.Errorf("ensure issue ID available: transaction is nil")
}
if id == "" {
return fmt.Errorf("ensure issue ID available: ID is empty")
}
key := issueCreateCoordinationKey(id)
if _, err := tx.ExecContext(ctx, "REPLACE INTO local_metadata (`key`, value) VALUES (?, ?)", key, strconv.FormatInt(FreshRowLock(), 10)); err != nil {
return fmt.Errorf("coordinate issue create: %w", err)
}
for _, table := range []string{"issues", "wisps"} {
var count int
if err := tx.QueryRowContext(ctx, "SELECT COUNT(*) FROM "+table+" WHERE id = ?", id).Scan(&count); err != nil {
return fmt.Errorf("check %s for issue %q: %w", table, id, err)
}
if count > 0 {
return fmt.Errorf("%w: %s", storage.ErrAlreadyExists, id)
}
}
return nil
}
func issueCreateCoordinationKey(id string) string {
sum := sha256.Sum256([]byte(id))
shard := uint16(sum[0])<<4 | uint16(sum[1])>>4
return fmt.Sprintf("issue-create/v1/%03x", shard)
}
View on GitHub (pinned to 71377f2769)
Solutions
- Match with errors.Is(err, storage.ErrAlreadyExists) and switch to update-or-skip logic for that ID
- Generate a fresh unique ID for the new issue and retry
- Check existence up front (or prefix IDs with a per-run unique token) before batch import
Example fix
// before
err := CreateIssueInTxWithResult(ctx, tx, issue) // panics on dup at call site
// after
if err := CreateIssueInTxWithResult(ctx, tx, issue); errors.Is(err, storage.ErrAlreadyExists) {
return skipOrCreateFreshID(issue)
} Defensive patterns
Strategy: type-guard
Validate before calling
var count int
row := db.QueryRow("SELECT (SELECT COUNT(*) FROM issues WHERE id=?) + (SELECT COUNT(*) FROM wisps WHERE id=?)", id, id)
row.Scan(&count)
// count > 0 means the create will be refused Type guard
func isAlreadyExists(err error) bool { return errors.Is(err, storage.ErrAlreadyExists) } Try / catch
if err := CreateIssueInTxWithResult(ctx, tx, issue); err != nil {
if isAlreadyExists(err) { /* skip, update, or mint a new ID */ }
return err
} Prevention
- Check both issues and wisps for ID collisions before import
- Use unique ID generation (prefix + timestamp/counter)
- Decide up-front whether imports skip or update existing IDs
When it happens
Trigger: CreateIssueInTxWithResult called with an ID that already exists in either issues or wisps — including a wisp holding the same ID as a would-be regular issue (the guard checks both tables).
Common situations: Re-importing an exported file without skip-existing logic; ID collisions between regular issues and wisps; concurrent workers generating the same ID.
Related errors
- %w: %w
- config storage-class.%s: %w
- failed to create issue: %w
- failed to resolve %s: %w
- cannot mark an issue as duplicate of itself
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/735c4e11b017105d.
Report an issue: GitHub.