gastownhall/beads · error

close %s: %w

Error message

close %s: %w

What it means

Wraps the underlying error from issueRepo.CloseChecked — the actual close-write against the issues/wisps table failed. The ID is interpolated for context and the storage error is chained as %w. This is the generic wrapper for storage-level close failures, distinct from the empty-id/actor guards and from the later reload failure.

Source

Thrown at internal/storage/domain/issue.go:1672

func (u *issueUseCaseImpl) CloseIssueChecked(ctx context.Context, id string, params CloseIssueParams, actor string, force bool) (CloseIssueResult, error) {
	return u.closeChecked(ctx, id, params, actor, force, false)
}

// CloseWispChecked is the wisp twin of CloseIssueChecked.
func (u *issueUseCaseImpl) CloseWispChecked(ctx context.Context, id string, params CloseIssueParams, actor string, force bool) (CloseIssueResult, error) {
	return u.closeChecked(ctx, id, params, actor, force, true)
}

func (u *issueUseCaseImpl) closeChecked(ctx context.Context, id string, params CloseIssueParams, actor string, force, useWisp bool) (CloseIssueResult, error) {
	if id == "" {
		return CloseIssueResult{}, fmt.Errorf("close: id must not be empty")
	}
	if actor == "" {
		return CloseIssueResult{}, fmt.Errorf("close: actor must not be empty")
	}
	row, err := u.issueRepo.CloseChecked(ctx, id, CloseRowParams{Reason: params.Reason, Session: params.Session}, actor, force)
	if err != nil {
		return CloseIssueResult{}, fmt.Errorf("close %s: %w", id, err)
	}
	issue, err := u.issueRepo.Get(ctx, id, IssueTableOpts{UseWispsTable: row.IsWisp || useWisp})
	if err != nil {
		return CloseIssueResult{}, fmt.Errorf("close %s: reload: %w", id, err)
	}
	return CloseIssueResult{Issue: issue, Closed: !row.AlreadyClosed, OpenChildren: row.OpenChildren}, nil
}

func (u *issueUseCaseImpl) close(ctx context.Context, id string, params CloseIssueParams, actor string, useWisp bool) (CloseIssueResult, error) {
	if id == "" {
		return CloseIssueResult{}, fmt.Errorf("close: id must not be empty")
	}
	if actor == "" {
		return CloseIssueResult{}, fmt.Errorf("close: actor must not be empty")
	}
	row, err := u.issueRepo.Close(ctx, id, CloseRowParams{Reason: params.Reason, Session: params.Session}, actor, IssueTableOpts{UseWispsTable: useWisp})
	if err != nil {
		return CloseIssueResult{}, fmt.Errorf("close %s: %w", id, err)

View on GitHub (pinned to 71377f2769)

Solutions

  1. Read the wrapped %w error: if not-found, verify the ID exists (bd show <id>).
  2. If already-closed, either treat as success in your flow or pass force=true.
  3. Check DB connectivity/locks and retry transient errors.
  4. Use the matching table variant — a wisp ID may need CloseWisp* rather than CloseIssue* depending on your lookup path.
Defensive patterns

Strategy: try-catch

Validate before calling

// pre-check existence and state before closing
issue, err := uc.GetIssue(ctx, id)
if err != nil {
    return fmt.Errorf("issue %s not found: %w", id, err)
}
if issue.Status == StatusClosed && !force {
    return fmt.Errorf("issue %s already closed; pass force to no-op", id)
}

Try / catch

if err != nil {
    switch {
    case errors.Is(err, ErrNotFound):
        return fmt.Errorf("close %s: no such issue; verify ID with bd show", id)
    case errors.Is(err, ErrAlreadyClosed):
        return nil // idempotent close
    case isTransientStorageErr(err):
        return retryWithBackoff(closeOp)
    default:
        return fmt.Errorf("close %s: %w", id, err)
    }
}

Prevention

When it happens

Trigger: CloseChecked call fails: issue not found in either table, already closed without force, DB write error, permission/driver error, context canceled during the write.

Common situations: Typo in issue ID (bd-9999 doesn't exist); trying to close an already-closed issue without force; database locked or offline; concurrent sync conflict.

Related errors


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