gastownhall/beads · error

issue must not be nil

Error message

issue must not be nil

What it means

createIssue (and its exported wrapper CreateIssue) rejects a nil *types.Issue pointer before doing any work. This is a programmer-error guard: passing nil means no issue data exists to persist, so the library returns a clear error instead of panicking on a nil dereference.

Source

Thrown at internal/storage/dolt/issues.go:31

	"go.opentelemetry.io/otel/metric"

	"github.com/steveyegge/beads/internal/idgen"
	"github.com/steveyegge/beads/internal/storage"
	"github.com/steveyegge/beads/internal/storage/issueops"
	"github.com/steveyegge/beads/internal/types"
)

// CreateIssue creates a new issue.
// Delegates SQL work to issueops; handles Dolt versioning for non-ephemeral issues.
func (s *DoltStore) CreateIssue(ctx context.Context, issue *types.Issue, actor string) error {
	return s.withCircuitWrite(ctx, func(ctx context.Context) error {
		return s.createIssue(ctx, issue, actor)
	})
}

func (s *DoltStore) createIssue(ctx context.Context, issue *types.Issue, actor string) error {
	if issue == nil {
		return fmt.Errorf("issue must not be nil")
	}

	// Route to wisps table if ephemeral, no-history, wisp-typed, or infra type.
	// A wisp_type is a claim of ephemerality: minted without the flag it lands
	// in the issues plane where no TTL, GC, or purge tier owns it.
	useWispsTable := issue.Ephemeral || issue.NoHistory || issue.WispType != "" || s.IsInfraTypeCtx(ctx, issue.IssueType)
	if useWispsTable && !issue.NoHistory {
		issue.Ephemeral = true // infra and wisp types get marked ephemeral (legacy behavior)
	}

	var result issueops.CreateIssueResult
	if err := s.withRetryTx(ctx, func(tx *sql.Tx) error {
		// SkipPrefixValidation matches legacy behavior: single-issue path does
		// not validate prefixes for explicit IDs.
		bc, err := issueops.NewBatchContext(ctx, tx, storage.BatchCreateOptions{
			SkipPrefixValidation: true,
		})
		if err != nil {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Ensure a non-nil *types.Issue with required fields (Title, etc.) is constructed before calling CreateIssue.
  2. Check the error return of whatever produced the issue pointer — a nil issue usually indicates an earlier ignored error.
  3. In wrappers/CLIs, validate decoded input and return a user-facing 'issue data required' message instead of passing nil through.

Example fix

// before
var issue *types.Issue // nil unless populated
json.Unmarshal(body, &issue) // may leave issue nil on empty body
store.CreateIssue(ctx, issue, actor) // "issue must not be nil"
// after
issue := &types.Issue{}
if err := json.Unmarshal(body, issue); err != nil { return err }
if issue.Title == "" { return fmt.Errorf("title required") }
store.CreateIssue(ctx, issue, actor)
Defensive patterns

Strategy: type-guard

Validate before calling

if issue == nil || issue.Title == "" { return fmt.Errorf("issue with non-empty title required") }
store.CreateIssue(ctx, issue, actor)

Type guard

func hasIssue(i *types.Issue) bool { return i != nil }

Try / catch

if err := store.CreateIssue(ctx, issue, actor); err != nil {
    if strings.Contains(err.Error(), "issue must not be nil") {
        return fmt.Errorf("no issue data provided (check upstream error handling)")
    }
    return err
}

Prevention

When it happens

Trigger: Calling store.CreateIssue(ctx, nil, actor) — e.g. a variable that failed to initialize, a function that returns (issue, err) where issue is nil on an ignored error path, or decoding empty input into a nil pointer.

Common situations: JSON payloads that deserialize to a nil issue when a required field check is skipped; refactored code paths where an earlier error was swallowed and the zero-value (nil) pointer flowed onward; test harnesses passing nil placeholders.

Related errors


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