gastownhall/beads · error · storage.ErrNotFound

%w: issue %s

Error message

%w: issue %s

What it means

getJournalIssueInTx returns storage.ErrNotFound wrapped with the issue ID when neither candidate issue table (issues or deleted issues) contains a snapshot row for the ID. It is the sentinel path for 'this journal event has no issue to snapshot'.

Source

Thrown at internal/storage/issueops/journal.go:411

		issue, err := getIssueFromTableInTx(ctx, tx, candidate.issueTable, candidate.labelTable, issueID)
		if errors.Is(err, storage.ErrNotFound) {
			continue
		}
		if err != nil {
			if optionalBlockedTable(candidate.issueTable) && isTableNotExistError(err) {
				continue
			}
			return nil, err
		}
		var blocked int
		//nolint:gosec // candidate.issueTable is one of the two hardcoded values above.
		if err := tx.QueryRowContext(ctx, fmt.Sprintf("SELECT is_blocked FROM %s WHERE id = ?", candidate.issueTable), issueID).Scan(&blocked); err != nil {
			return nil, fmt.Errorf("journal: read is_blocked for %s: %w", issueID, err)
		}
		issue.IsBlocked = blocked != 0
		return issue, nil
	}
	return nil, fmt.Errorf("%w: issue %s", storage.ErrNotFound, issueID)
}

// insertEventRow performs the actual INSERT. It is the ONE seam both write
// plumbings funnel through, so the seq mechanism cannot drift between them. A
// nil issue is stored as SQL NULL (deletes); a nil dep is stored as SQL NULL
// (non-dependency ops). ts is the insert time, stamped inside the committing
// transaction. actor is stored as-is — "" for the genuinely unattributable
// paths — in the NOT NULL DEFAULT ” actor column.
func insertEventRow(ctx context.Context, tx DBTX, op EventOp, issueID string, issue *types.Issue, dep *EventDep, comment *EventComment, actor string) error {
	var issueJSON any
	if issue != nil {
		b, err := json.Marshal(issue)
		if err != nil {
			return fmt.Errorf("journal: marshal issue %s: %w", issueID, err)
		}
		issueJSON = string(b)
	}
	var depJSON any

View on GitHub (pinned to 71377f2769)

Solutions

  1. Verify the issue exists: bd show <id> before mutating
  2. Create the issue first if it never existed
  3. Check for concurrent deletion; re-sync or skip the event
  4. errors.Is(err, storage.ErrNotFound) to branch on this case explicitly

Example fix

// before
if err := ops.RecordEventInTx(ctx, tx, op, id, nil, nil); err != nil { return err }
// after
if err := ops.RecordEventInTx(ctx, tx, op, id, nil, nil); err != nil {
    if errors.Is(err, storage.ErrNotFound) { return fmt.Errorf("skipping %s: issue gone", id) }
    return err
}
Defensive patterns

Strategy: type-guard

Validate before calling

var exists bool
_ = tx.QueryRowContext(ctx, "SELECT EXISTS(SELECT 1 FROM issues WHERE id = ?)", id).Scan(&exists)
if !exists { return fmt.Errorf("cannot journal unknown issue %s", id) }

Type guard

func isIssueNotFound(err error) bool {
    return errors.Is(err, storage.ErrNotFound)
}

Try / catch

if err := ops.RecordEventInTx(ctx, tx, op, id, nil, nil); err != nil {
    if isIssueNotFound(err) { return skipEvent(id) }
    return err
}

Prevention

When it happens

Trigger: Recording any journal event (RecordEventInTx, RecordDepEventInTx, RecordCommentEventInTx) for an issue ID absent from both issues and the tombstone table — e.g. the issue was never created, was purged, or the ID is mistyped.

Common situations: Sync/import scripts referencing IDs from another database; commenting on or updating an issue deleted by another worker; tests using fabricated IDs; cross-repo ID confusion after re-cloning.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/1d47fd413493ec3f. Report an issue: GitHub.