gastownhall/beads · error

failed to close issue: %w

Error message

failed to close issue: %w

What it means

This error wraps the underlying driver/SQL error returned when the UPDATE that marks an issue as closed fails inside a transaction. closeIssueInTx executes an UPDATE on the issues table setting status=closed, closed_at, close_reason, and session/lock columns; any failure from the database layer is surfaced here. It preserves the driver error via %w so callers can inspect the root cause with errors.Is/As.

Source

Thrown at internal/storage/issueops/close.go:345

		affectedIssues, affectedWisps, aerr = AffectedByStatusChangeInTx(ctx, tx, id)
	}
	if aerr != nil {
		return nil, fmt.Errorf("affected by close for %s: %w", id, aerr)
	}

	now := time.Now().UTC()

	// row_lock is rewritten on close so a concurrent reclaim (which also rewrites
	// row_lock) collides on this cell and is forced to conflict-and-retry rather
	// than silently cell-merging a revert-to-ready over a completed close (see
	// lease.go). The lease row is deleted below: a closed issue holds no lease.
	result, err := tx.ExecContext(ctx, fmt.Sprintf(`
		UPDATE %s SET status = ?, closed_at = ?, updated_at = ?, close_reason = ?, closed_by_session = ?,
			row_lock = ?
		WHERE id = ? AND status != ?
	`, issueTable), types.StatusClosed, now, now, reason, session, freshRowLock(), id, types.StatusClosed)
	if err != nil {
		return nil, fmt.Errorf("failed to close issue: %w", err)
	}

	rows, err := result.RowsAffected()
	if err != nil {
		return nil, fmt.Errorf("failed to get rows affected: %w", err)
	}
	if rows == 0 {
		var status string
		qerr := tx.QueryRowContext(ctx,
			fmt.Sprintf(`SELECT status FROM %s WHERE id = ?`, issueTable), id,
		).Scan(&status)
		if qerr == sql.ErrNoRows {
			return nil, fmt.Errorf("%w: issue %s", storage.ErrNotFound, id)
		}
		if qerr != nil {
			return nil, fmt.Errorf("failed to check issue existence: %w", qerr)
		}
		if types.Status(status) == types.StatusClosed {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Inspect the wrapped driver error with errors.Is/As to identify the root cause (context deadline, deadlock, connection refused) and address that first.
  2. Retry the whole operation with a fresh transaction; the tx is aborted after an exec error so partial retry inside the same tx is impossible.
  3. Check connectivity and server health (bd dolt push/ping the DB); verify the context timeout is generous enough.
  4. Verify the schema matches the installed beads version (run bd doctor / migrations) — missing columns cause UPDATE errors.

Example fix

// before
result, err := tx.ExecContext(ctx, closeSQL, args...)
if err != nil {
    return nil, fmt.Errorf("failed to close issue: %w", err)
}
// after
result, err := tx.ExecContext(ctx, closeSQL, args...)
if err != nil {
    if errors.Is(err, context.DeadlineExceeded) {
        return nil, fmt.Errorf("failed to close issue %s: transaction timed out, retry with fresh tx: %w", id, err)
    }
    return nil, fmt.Errorf("failed to close issue: %w", err)
}
Defensive patterns

Strategy: try-catch

Try / catch

if err := store.CloseIssue(ctx, id, actor, reason); err != nil {
    var driverErr *mysql.MySQLError // or driver-specific type
    if errors.As(err, &driverErr) {
        log.Printf("driver error %d on close: %v", driverErr.Number, err)
    }
    if errors.Is(err, context.DeadlineExceeded) {
        // retry once with a fresh context/transaction
    }
    return err
}

Prevention

When it happens

Trigger: Any failure of tx.ExecContext running the close UPDATE: connection loss mid-transaction, constraint violation, locked table, malformed row_lock value, or the transaction already having been rolled back or timed out via its context.

Common situations: Database restarted or connection dropped during a long transaction; context deadline exceeded on a slow Dolt/MySQL server; deadlock with another concurrent writer holding locks on the same issue row; schema drift after a version upgrade removing a column like close_reason or closed_by_session.

Related errors


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