gastownhall/beads · warning

ErrAlreadyClaimed

ErrAlreadyClaimed

Error message

issue already claimed

What it means

ErrAlreadyClaimed is returned when attempting to claim an issue that is already claimed by another user. The error message contains the current assignee. ClaimConflictError wraps this sentinel while reporting the current assignee and status read inside the transaction that lost the compare-and-set, so errors.Is still matches.

Source

Thrown at issueops/errors.go:12

package issueops

import (
	"errors"
	"fmt"

	"github.com/steveyegge/beads/beadserrors"
)

// ErrAlreadyClaimed is returned when attempting to claim an issue that is already
// claimed by another user. The error message contains the current assignee.
var ErrAlreadyClaimed = errors.New("issue already claimed")

// ErrNotClaimable is returned when attempting to claim an issue that is not in a
// claimable state, such as closed, deferred, or already in progress without the
// same actor owning the claim.
var ErrNotClaimable = errors.New("issue not claimable")

// ClaimConflictError reports the state that refused a claim — the current
// assignee and status, read inside the same transaction that lost the
// compare-and-set. It wraps the refusal rather than replacing it, so the
// sentinel still matches, the refusal's carefully-worded prose survives
// byte-for-byte, and a caller can classify the conflict from typed fields
// instead of parsing that prose.
//
// Err is set by every implementation that returns this type; a Claimer whose
// same-transaction re-read fails returns the bare refusal instead.
type ClaimConflictError struct {
	// IssueID names the issue that refused the claim.
	IssueID string

View on GitHub (pinned to 71377f2769)

Solutions

  1. Pick a different, unclaimed issue from the pool.
  2. Coordinate with the current assignee (named in the error/ClaimConflictError fields) to have them release the claim.
  3. Have the current holder unclaim, then retry the claim.
  4. Use errors.Is(err, issueops.ErrAlreadyClaimed) and read ClaimConflictError fields to surface who holds it.

Example fix

// before
err := claimer.Claim(ctx, req) // fails: already claimed by alice

// after
var conflict *issueops.ClaimConflictError
if errors.As(err, &conflict) {
    fmt.Printf("issue held by %s (status %s)\n", conflict.Assignee, conflict.Status)
} else if errors.Is(err, issueops.ErrAlreadyClaimed) {
    // choose another issue
}
Defensive patterns

Strategy: type-guard

Validate before calling

iss, _ := store.GetIssue(ctx, id)
if iss.Assignee != "" && iss.Assignee != actor {
    // skip: already claimed by someone else
}

Type guard

var conflict *issueops.ClaimConflictError
if errors.As(err, &conflict) {
    holder := conflict.Assignee
    _ = holder
}

Try / catch

if errors.Is(err, issueops.ErrAlreadyClaimed) {
    var c *issueops.ClaimConflictError
    errors.As(err, &c) // show current assignee from typed fields
}

Prevention

When it happens

Trigger: Calling the claimer (claim flow / issue operations update with claim semantics) on an issue whose assignee is set to someone other than the requesting actor; two actors racing to claim the same pool-assigned issue; RunClaimer test paths exercising foreign-holder refusals.

Common situations: Team members grabbing work from a shared pool simultaneously; a stale local view of an unassigned issue; automated agents competing for tasks.

Related errors


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