{"record":{"id":"02dcb2f4825c9643","repo":"gastownhall/beads","slug":"w-issue-s-02dcb2","errorCode":null,"errorMessage":"%w: issue %s","messagePattern":"%w: issue (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/uow/issue_claimer.go","lineNumber":71,"sourceCode":"\tif request.Actor == \"\" || request.IssueID == \"\" {\n\t\treturn publicops.ClaimResult{}, validationError(fmt.Errorf(\"claim: actor and issue ID must not be empty\"))\n\t}\n\treturn RunTxResult(ctx, c.provider, func(ctx context.Context, uw UnitOfWork) (publicops.ClaimResult, string, error) {\n\t\tuc := uw.IssueUseCase()\n\t\tclaimed, err := uc.ClaimIssue(ctx, request.IssueID, request.Actor)\n\t\tif err != nil {\n\t\t\treturn publicops.ClaimResult{}, \"\", classifyClaimError(ctx, uc, request.IssueID, err)\n\t\t}\n\t\t// Read back INSIDE this transaction, so the result describes the row\n\t\t// this CAS wrote and not a later writer's.\n\t\tissue, err := uc.GetIssue(ctx, request.IssueID)\n\t\tif err != nil {\n\t\t\treturn publicops.ClaimResult{}, \"\", err\n\t\t}\n\t\tif issue == nil {\n\t\t\t// A miss with a nil error is the other shape a not-found takes at\n\t\t\t// this seam; normalize it rather than dereferencing nil.\n\t\t\treturn publicops.ClaimResult{}, \"\", fmt.Errorf(\"%w: issue %s\", publicops.ErrNotFound, request.IssueID)\n\t\t}\n\t\tif claimed.AlreadyClaimed {\n\t\t\t// The idempotent re-claim: the CAS matched no row because there\n\t\t\t// was nothing to change. An empty commit message tells\n\t\t\t// RunTxResult to skip the commit, so a polling caller cannot mint\n\t\t\t// an empty storage commit per call.\n\t\t\treturn publicops.ClaimResult{Issue: issue}, \"\", nil\n\t\t}\n\t\treturn publicops.ClaimResult{Issue: issue, Changed: true}, storageissueops.ClaimCommitMessage(request.IssueID, request.Actor), nil\n\t})\n}\n\n// classifyClaimError normalizes what a lost or impossible claim reports.\n//\n// A refusal gains the assignee and status read in THIS transaction, so a\n// caller classifies the conflict from typed fields instead of matching\n// substrings in the message; when that read fails the refusal stands\n// unadorned, because reporting the read's failure would replace a precise","sourceCodeStart":53,"sourceCodeEnd":89,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/uow/issue_claimer.go#L53-L89","documentation":"After ClaimIssue succeeds but returns a nil issue, the claimer normalizes the miss to a wrapped publicops.ErrNotFound. This covers the shape where the use case signals 'not found' by returning (nil, nil) instead of an error, preventing a nil dereference downstream. The wrapped message is `issue <id>`, joined to the ErrNotFound sentinel with %w so callers can errors.Is against it.","triggerScenarios":"Claiming (or checking) an issue ID that does not exist, where the underlying use case returns a nil issue with a nil error.","commonSituations":"Stale ID cached from another machine after the issue was deleted; typo in the issue ID; polling loop running after the issue was closed and reaped.","solutions":["Confirm the issue ID exists (e.g. `bd show <id>`) before claiming.","Handle the error with errors.Is(err, publicops.ErrNotFound) and treat it as a benign miss.","If polling, remove the issue from the work set when ErrNotFound is returned."],"exampleFix":"// before\nres, err := c.Claim(ctx, req)\nif err != nil { return err } // assumes all errors are fatal\n// after\nif errors.Is(err, publicops.ErrNotFound) { return nil // issue gone, skip }\nreturn err","handlingStrategy":"type-guard","validationCode":"if _, err := uow.GetIssue(ctx, id); errors.Is(err, publicops.ErrNotFound) { return nil }","typeGuard":"func isNotFound(err error) bool { return errors.Is(err, publicops.ErrNotFound) }","tryCatchPattern":"if err != nil {\n    if errors.Is(err, publicops.ErrNotFound) { return nil // benign miss }\n    return err\n}","preventionTips":["Always errors.Is against publicops.ErrNotFound, never string-match messages.","Treat not-found in polling loops as skip, not failure.","Verify IDs exist before long-running claim loops."],"tags":["not-found","claims","sentinel-error"],"backgroundTag":"resource-not-found","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}