{"record":{"id":"3be9d31c0809c4ad","repo":"gastownhall/beads","slug":"failed-to-close-issue-s","errorCode":null,"errorMessage":"failed to close issue: %s","messagePattern":"failed to close issue: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/close.go","lineNumber":366,"sourceCode":"\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 {\n\t\t\treturn &CloseResult{IsWisp: isWisp, AlreadyClosed: true}, nil\n\t\t}\n\t\treturn nil, fmt.Errorf(\"failed to close issue: %s\", id)\n\t}\n\n\t// A closed issue holds no lease (no-op for wisps, which are never leased).\n\tif err := DeleteLeaseInTx(ctx, tx, id); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif recordEvent {\n\t\tif err := RecordEventInTable(ctx, tx, eventTable, id, types.EventClosed, actor, reason); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to record event: %w\", err)\n\t\t}\n\t}\n\n\trecompute, err := RecomputeIsBlockedInTxWithResult(ctx, tx, affectedIssues, affectedWisps)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"recompute is_blocked after close for %s: %w\", id, err)\n\t}\n","sourceCodeStart":348,"sourceCodeEnd":384,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/close.go#L348-L384","documentation":"This error is returned when the close UPDATE matched zero rows and the follow-up check confirms the issue exists but is not in 'closed' status — i.e. the row could not be closed for a reason other than being already closed. This is a logical/state failure rather than a driver failure; the issue ID is included for direct reporting. Note: if the existing status were 'closed', the function returns AlreadyClosed success instead.","triggerScenarios":"UPDATE matched 0 rows, the issue exists with a status other than closed (e.g. a wisp status or a custom status value) so the WHERE status != 'closed' predicate excluded it, yet the code path treats it as unclosable; or a concurrent transaction deleted/re-created the row between UPDATE and SELECT.","commonSituations":"Racing writers: one goroutine deletes or mutates the issue while another closes it; data mutated by an external tool so the status holds an unexpected value; wisps being closed through the issue-close path instead of the wisp path.","solutions":["Re-run the close; if transient (concurrent delete/recreate), the retry will see a consistent state.","Check the issue's current status with `bd show <id>`; use the appropriate command for its state (e.g. wisp-specific handling).","Serialize conflicting writers — don't close and delete the same issue concurrently.","If a custom status value is present, correct it or upgrade beads so the close predicate covers it."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// verify the issue is in a closable state first\nvar status string\nif err := db.QueryRow(\"SELECT status FROM issues WHERE id = ?\", id).Scan(&status); err != nil {\n    return err\n}\nif types.Status(status) == types.StatusClosed {\n    return nil // nothing to do\n}","typeGuard":null,"tryCatchPattern":"err := store.CloseIssue(ctx, id, actor, reason)\nif err != nil && strings.HasPrefix(err.Error(), \"failed to close issue: \") {\n    // non-driver, non-notfound state conflict: inspect current status\n    st, _ := getStatus(ctx, id)\n    return fmt.Errorf(\"cannot close %s (status=%s)\", id, st)\n}","preventionTips":["Don't close and delete/mutate the same issue concurrently — serialize writes.","Check current status before issuing the close; treat already-closed as success.","Route wisp records through wisp-specific paths, not the issue close path.","Avoid external tools writing arbitrary status values into the issues table."],"tags":["state-conflict","concurrency","issue-tracking","logic"],"backgroundTag":"issue-close-conflict","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}