gastownhall/beads · error

failed to get issue for update: %w

Error message

failed to get issue for update: %w

What it means

readIssueAndResolveMergeOps wraps a failure from GetIssueInTx when it reads the pre-update row inside the mutation transaction. If the current row cannot be loaded, merge operations (metadata edits, note appends) cannot be resolved, so the whole update is aborted with this wrapped error preserving the underlying cause via %w.

Source

Thrown at internal/storage/issueops/update.go:1018

			if !ok {
				return nil, fmt.Errorf("%s must be a list of strings, got element %T", op, item)
			}
			out = append(out, s)
		}
		return out, nil
	default:
		return nil, fmt.Errorf("%s must be a list of strings, got %T", op, value)
	}
}

// readIssueAndResolveMergeOps reads the pre-update row in-transaction and folds
// any merge-operation keys (metadata edits, note appends) into concrete column
// values against that row, returning the row and the rewritten update map. It
// keeps the read-merge-write plumbing off updateIssueInTx's already-large body.
func readIssueAndResolveMergeOps(ctx context.Context, tx DBTX, id string, updates map[string]interface{}) (*types.Issue, map[string]interface{}, error) {
	oldIssue, err := GetIssueInTx(ctx, tx, id)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to get issue for update: %w", err)
	}
	resolved, err := ResolveMergeOps(oldIssue, updates)
	if err != nil {
		return nil, nil, err
	}
	return oldIssue, resolved, nil
}

// RecordFullEventInTable records an event with both old and new values.
func RecordFullEventInTable(ctx context.Context, tx DBTX, table, issueID string, eventType types.EventType, actor, oldValue, newValue string) error {
	return InsertDerivedEvent(ctx, tx, table, AuxEvent{
		IssueID:   issueID,
		EventType: eventType,
		Actor:     actor,
		OldValue:  str(oldValue),
		NewValue:  str(newValue),
	})
}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Verify the issue ID exists (bd show / GetIssue) before issuing the update
  2. Check errors.Is(err, storage.ErrNotFound) to distinguish a missing issue from a real DB failure
  3. If ErrNotFound, treat as a race: re-fetch state and decide whether to recreate or surface 'issue no longer exists'
  4. Inspect the wrapped cause (%w) for connection/permission problems and retry transient errors

Example fix

// before
_, err := store.Update(ctx, id, updates) // panics/logs generically
// after
_, err := store.Update(ctx, id, updates)
if err != nil && errors.Is(err, storage.ErrNotFound) {
    return fmt.Errorf("issue %s no longer exists; refresh and retry", id)
}
Defensive patterns

Strategy: try-catch

Validate before calling

if _, err := store.GetIssue(ctx, id); err != nil { return fmt.Errorf("issue %s unavailable before update: %w", id, err) }

Try / catch

_, err := store.Update(ctx, id, updates)
if err != nil {
    switch {
    case errors.Is(err, storage.ErrNotFound):
        return fmt.Errorf("issue %s gone; refresh before updating", id)
    default:
        return err // inspect wrapped DB cause
    }
}

Prevention

When it happens

Trigger: Calling updateIssueInTx (via Update/ExecuteUpdate) with a merge-op key in updates while the issue id does not exist, the row was deleted concurrently, or the underlying SELECT fails (connection, permissions, schema).

Common situations: Updating an issue that was already deleted in another session; transient DB connection loss mid-transaction; mistyped issue ID; replica/primary mismatch in distributed Dolt setups.

Related errors


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