gastownhall/beads · info · storage.ErrNotFound
%w: issue %s
Error message
%w: issue %s
What it means
GetIssueInTx looks up the ID in the issues table, then in wisps; if both return storage.ErrNotFound, it wraps storage.ErrNotFound with the issue ID. This is the canonical not-found signal — detect it with errors.Is. It means no issue or wisp with that ID exists in this database.
Source
Thrown at internal/storage/issueops/get_issue.go:33
// GetIssueInTx retrieves a single issue by ID within an existing transaction,
// including its labels. Automatically routes to the wisps/wisp_labels tables
// if the ID is an active wisp. Returns storage.ErrNotFound (wrapped) if the
// issue does not exist in either table.
func GetIssueInTx(ctx context.Context, tx DBTX, id string) (*types.Issue, error) {
issue, err := getIssueFromTableInTx(ctx, tx, "issues", "labels", id)
if err == nil {
return issue, nil
}
if !errors.Is(err, storage.ErrNotFound) {
return nil, err
}
issue, err = getIssueFromTableInTx(ctx, tx, "wisps", "wisp_labels", id)
if err == nil {
return issue, nil
}
if errors.Is(err, storage.ErrNotFound) {
return nil, fmt.Errorf("%w: issue %s", storage.ErrNotFound, id)
}
return nil, err
}
// missingOptionalIssueTable reports whether err is the absence of the optional
// issue plane the hydration query just read. The hydration FROM clause also
// carries sqlbuild.LeaseJoin, so a blanket table-not-exist check here folds a
// missing leases table into "row absent" — a wrong answer, not an empty one.
func missingOptionalIssueTable(err error, issueTable string) bool {
return optionalBlockedTable(issueTable) && dberrors.IsMissingTable(err, issueTable)
}
func getIssueFromTableInTx(ctx context.Context, tx DBTX, issueTable, labelTable, id string) (*types.Issue, error) {
//nolint:gosec // G201: issueTable is a hardcoded literal supplied by GetIssueInTx ("issues" or "wisps")
row := tx.QueryRowContext(ctx, fmt.Sprintf(`SELECT %s FROM %s %s WHERE id = ?`,
IssueSelectColumns, issueTable, sqlbuild.LeaseJoin(issueTable)), id)
issue, err := ScanIssueFrom(row)
if err == sql.ErrNoRows || missingOptionalIssueTable(err, issueTable) {View on GitHub (pinned to 71377f2769)
Solutions
- Verify the ID with bd show <id> or by listing issues before the lookup
- Confirm you are connected to the database where the issue was created
- Branch on errors.Is(err, storage.ErrNotFound) to handle absence gracefully
- Re-create the issue if it was legitimately deleted
Example fix
// before
issue, err := issueops.GetIssueInTx(ctx, tx, id) // hard-fails on unknown ID
// after
issue, err := issueops.GetIssueInTx(ctx, tx, id)
if errors.Is(err, storage.ErrNotFound) {
return fmt.Errorf("issue %q does not exist in this database", id)
}
if err != nil {
return err
} Defensive patterns
Strategy: type-guard
Validate before calling
// pre-check existence without labels/lease join var exists bool err := tx.QueryRowContext(ctx, `SELECT COUNT(*) > 0 FROM issues WHERE id = ?`, id).Scan(&exists)
Type guard
func isIssueNotFound(err error) bool {
return errors.Is(err, storage.ErrNotFound)
} Try / catch
issue, err := issueops.GetIssueInTx(ctx, tx, id)
if isIssueNotFound(err) {
return fmt.Errorf("issue %q not found; run 'bd list' to see available IDs", id)
}
if err != nil {
return err
} Prevention
- Always branch on errors.Is(err, storage.ErrNotFound) before generic handling
- Confirm the database/clone you are querying is the one where the ID was created
- Use full IDs (or validate prefix expansion) instead of hand-typed partial IDs
When it happens
Trigger: Calling GetIssueInTx (directly or via ClaimIssueInTx, ClaimReadyIssueInTx, GetNewlyUnblockedByCloseInTx, buildDependencyTreeInTx, ExecuteUpdate) with an ID that exists in neither issues nor wisps.
Common situations: Typo'd or truncated issue ID; operating on a different database/clone than where the issue was created; issue deleted by another process; prefix-matched ID that resolved incorrectly.
Related errors
- no store is open for this workspace
- molecule %s not found: %w
- issue %s not found
- epic '%s' not found
- issue '%s' not found
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/228c105ea5aad374.
Report an issue: GitHub.