gastownhall/beads · error

DeleteIssue: id must not be empty

Error message

DeleteIssue: id must not be empty

What it means

DeleteIssue validates its required id argument before doing any work and returns this error when the id is an empty string. It is a pure input-validation guard in the use-case layer; no storage call is made. The caller passed an uninitialized or stripped issue ID.

Source

Thrown at internal/storage/domain/issue_delete.go:31

// DeleteBlockedError is the refusal returned by deleteMany when
// EnforceCascadePolicy is on, Cascade and Force are both off, and an issue in
// the deletion set has dependents outside it. The message mirrors classic
// (embedded) delete's refusal so both planes speak the same language.
type DeleteBlockedError struct {
	// IssueID is the first issue in the requested deletion set (request order)
	// found to have external dependents.
	IssueID string
	// Dependents are that issue's dependents outside the deletion set, sorted.
	Dependents []string
}

func (e *DeleteBlockedError) Error() string {
	return fmt.Sprintf("issue %s has dependents not in deletion set; use --cascade to delete them or --force to orphan them", e.IssueID)
}

func (u *issueUseCaseImpl) DeleteIssue(ctx context.Context, id, actor string) (DeleteIssuesResult, error) {
	if id == "" {
		return DeleteIssuesResult{}, fmt.Errorf("DeleteIssue: id must not be empty")
	}
	return u.deleteMany(ctx, DeleteIssuesParams{
		IDs:                  []string{id},
		Cascade:              true,
		UpdateTextReferences: true,
	}, actor)
}

func (u *issueUseCaseImpl) DeleteWisp(ctx context.Context, id, actor string) (DeleteIssuesResult, error) {
	if id == "" {
		return DeleteIssuesResult{}, fmt.Errorf("DeleteWisp: id must not be empty")
	}
	return u.deleteMany(ctx, DeleteIssuesParams{
		IDs:                  []string{id},
		Cascade:              true,
		UpdateTextReferences: true,
	}, actor)
}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Pass the actual issue ID (e.g. "bd-42") to DeleteIssue instead of an empty string
  2. Check where the ID is sourced (config, JSON, lookup result) and handle the missing-ID case before calling
  3. Add a caller-side check: if id == "" return a clear user-facing message before invoking the API

Example fix

// before
res, err := uc.DeleteIssue(ctx, issue.ID, actor) // issue.ID may be ""
// after
if issue.ID == "" {
	return fmt.Errorf("cannot delete: no issue ID provided")
}
res, err := uc.DeleteIssue(ctx, issue.ID, actor)
Defensive patterns

Strategy: validation

Validate before calling

func deleteIssue(id, actor string) error {
	if strings.TrimSpace(id) == "" {
		return fmt.Errorf("refusing to delete: issue id is empty")
	}
	_, err := uc.DeleteIssue(ctx, id, actor)
	return err
}

Type guard

func hasID(id string) bool { return strings.TrimSpace(id) != "" }

Try / catch

res, err := uc.DeleteIssue(ctx, id, actor)
if err != nil && strings.Contains(err.Error(), "id must not be empty") {
	return fmt.Errorf("caller bug: empty issue id passed to DeleteIssue")
}

Prevention

When it happens

Trigger: Calling DeleteIssue(ctx, "", actor) — e.g. an issue ID variable that was never populated, a struct field left zero-valued, or a string-trimming bug that reduced the ID to "".

Common situations: Scripts parsing issue IDs from JSON or CLI output where the key is missing; agent code building delete calls from empty lookup results; passing a zero-value string from a struct.

Related errors


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