gastownhall/beads · error
db: Update %s: %w
Error message
db: Update %s: %w
What it means
Wraps the case where Update reads the prior issue row and gets sql.ErrNoRows — the target issue does not exist. Beads deliberately re-wraps ErrNoRows with the 'db: Update %s: %w' prefix so callers can still detect absence via errors.Is(err, sql.ErrNoRows) while retaining the ID context.
Source
Thrown at internal/storage/domain/db/issue.go:172
if val, ok := raw.(string); ok {
if err := types.CheckFieldLen(field, val); err != nil {
return err
}
}
}
}
table := pickIssueTable(opts.UseWispsTable)
mergeOps := issueops.HasMergeOps(updates)
// Read the prior row once. Status and merge updates need it for their
// transaction-local resolution, and every update uses it to suppress true
// no-ops before changing row_lock or recording an event.
oldIssue, err := r.Get(ctx, id, opts)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return fmt.Errorf("db: Update %s: %w", id, sql.ErrNoRows)
}
return fmt.Errorf("db: Update %s: read old issue: %w", id, err)
}
// Resolve read-merge-write operation keys (issueops.OpMergeMetadata,
// OpSetMetadata, OpUnsetMetadata, OpAppendNotes) into concrete column
// values inside the mutation transaction, mirroring the embedded path
// (issueops.updateIssueInTx). Callers must pass the OPERATION, never a
// value pre-merged from an earlier read: this runner is a Dolt sql-server
// session where FOR UPDATE is a parse-only no-op, so a stale-snapshot merge
// is only made safe by Dolt's commit-time conflict detection plus the
// caller redoing the whole unit of work on a serialization failure — and
// that redo re-runs this in-transaction resolution against the winner's
// committed row.
if mergeOps {
resolved, err := issueops.ResolveMergeOps(oldIssue, updates)
if err != nil {
return fmt.Errorf("db: Update %s: %w", id, err)View on GitHub (pinned to 71377f2769)
Solutions
- Confirm the issue exists with bd show <id> or a Get call before updating
- Handle errors.Is(err, sql.ErrNoRows) explicitly in the caller as 'not found', not as an infrastructure fault
- Check whether the ID belongs to the wisps vs persistent table and pass matching IssueTableOpts
- Refresh any cached ID lists after concurrent operations
Example fix
// before
if err := repo.Update(ctx, id, updates, actor, opts); err != nil { return err }
// after
if err := repo.Update(ctx, id, updates, actor, opts); err != nil {
if errors.Is(err, sql.ErrNoRows) { return ErrIssueNotFound }
return err
} Defensive patterns
Strategy: try-catch
Validate before calling
if id == "" { return fmt.Errorf("issue ID required") }
if _, err := repo.Get(ctx, id, opts); err != nil {
if errors.Is(err, sql.ErrNoRows) { return ErrIssueNotFound }
return err
} Type guard
func isIssueNotFound(err error) bool { return errors.Is(err, sql.ErrNoRows) } Try / catch
if err := repo.Update(ctx, id, updates, actor, opts); err != nil {
if isIssueNotFound(err) {
log.Warnf("issue %s vanished; skipping update", id)
return ErrIssueNotFound
}
return err
} Prevention
- Always treat sql.ErrNoRows as not-found, not infra failure
- Match IssueTableOpts to the table the ID lives in (wisp vs persistent)
- Re-fetch ID lists after concurrent deletions
- Validate user-supplied IDs
When it happens
Trigger: Calling Update with an issue ID not present in the table: already-deleted issue, typo in ID, wrong table (ephemeral vs persistent mismatch), or a race where another process deleted the row between fetch and update.
Common situations: Stale ID from a cached list after concurrent deletion; passing a wisp ID where the persistent table is queried; user-supplied ID with a typo.
Related errors
- db: Update %s: read old issue: %w
- %w: issue %s
- db: MovePersistence %s: get issue: %w
- db: MovePersistence %s: %w
- db: Claim %s: read old issue: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/e2c6c0e3308317d0.
Report an issue: GitHub.