{"record":{"id":"f10d3112a97d3449","repo":"gastownhall/beads","slug":"failed-to-close-issue-w","errorCode":null,"errorMessage":"failed to close issue: %w","messagePattern":"failed to close issue: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/close.go","lineNumber":345,"sourceCode":"\t\taffectedIssues, affectedWisps, aerr = AffectedByStatusChangeInTx(ctx, tx, id)\n\t}\n\tif aerr != nil {\n\t\treturn nil, fmt.Errorf(\"affected by close for %s: %w\", id, aerr)\n\t}\n\n\tnow := time.Now().UTC()\n\n\t// row_lock is rewritten on close so a concurrent reclaim (which also rewrites\n\t// row_lock) collides on this cell and is forced to conflict-and-retry rather\n\t// than silently cell-merging a revert-to-ready over a completed close (see\n\t// lease.go). The lease row is deleted below: a closed issue holds no lease.\n\tresult, err := tx.ExecContext(ctx, fmt.Sprintf(`\n\t\tUPDATE %s SET status = ?, closed_at = ?, updated_at = ?, close_reason = ?, closed_by_session = ?,\n\t\t\trow_lock = ?\n\t\tWHERE id = ? AND status != ?\n\t`, issueTable), types.StatusClosed, now, now, reason, session, freshRowLock(), id, types.StatusClosed)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to close issue: %w\", err)\n\t}\n\n\trows, err := result.RowsAffected()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get rows affected: %w\", err)\n\t}\n\tif rows == 0 {\n\t\tvar status string\n\t\tqerr := tx.QueryRowContext(ctx,\n\t\t\tfmt.Sprintf(`SELECT status FROM %s WHERE id = ?`, issueTable), id,\n\t\t).Scan(&status)\n\t\tif qerr == sql.ErrNoRows {\n\t\t\treturn nil, fmt.Errorf(\"%w: issue %s\", storage.ErrNotFound, id)\n\t\t}\n\t\tif qerr != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to check issue existence: %w\", qerr)\n\t\t}\n\t\tif types.Status(status) == types.StatusClosed {","sourceCodeStart":327,"sourceCodeEnd":363,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/close.go#L327-L363","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Inspect the wrapped driver error with errors.Is/As to identify the root cause (context deadline, deadlock, connection refused) and address that first.","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.","Check connectivity and server health (bd dolt push/ping the DB); verify the context timeout is generous enough.","Verify the schema matches the installed beads version (run bd doctor / migrations) — missing columns cause UPDATE errors."],"exampleFix":"// before\nresult, err := tx.ExecContext(ctx, closeSQL, args...)\nif err != nil {\n    return nil, fmt.Errorf(\"failed to close issue: %w\", err)\n}\n// after\nresult, err := tx.ExecContext(ctx, closeSQL, args...)\nif err != nil {\n    if errors.Is(err, context.DeadlineExceeded) {\n        return nil, fmt.Errorf(\"failed to close issue %s: transaction timed out, retry with fresh tx: %w\", id, err)\n    }\n    return nil, fmt.Errorf(\"failed to close issue: %w\", err)\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"if err := store.CloseIssue(ctx, id, actor, reason); err != nil {\n    var driverErr *mysql.MySQLError // or driver-specific type\n    if errors.As(err, &driverErr) {\n        log.Printf(\"driver error %d on close: %v\", driverErr.Number, err)\n    }\n    if errors.Is(err, context.DeadlineExceeded) {\n        // retry once with a fresh context/transaction\n    }\n    return err\n}","preventionTips":["Always pass a context with an adequate timeout to close operations.","Retry the whole transaction on transient driver errors; never retry inside the aborted tx.","Keep beads and the storage driver/schema versions aligned (run bd doctor).","Monitor DB connectivity and avoid concurrent writers on the same issue row."],"tags":["database","transaction","sql","error-wrapping"],"backgroundTag":"sql-update-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}