gastownhall/beads · error

reopen %s: %w

Error message

reopen %s: %w

What it means

This error wraps any failure from u.issueRepo.Reopen in the reopen() use case — the storage-level state transition from closed back to open failed. The ID is embedded in the message and the underlying repository error (not-found, storage failure) is chained via %w.

Source

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

func (u *issueUseCaseImpl) ReopenIssue(ctx context.Context, id string, params ReopenIssueParams, actor string) (ReopenIssueResult, error) {
	return u.reopen(ctx, id, params, actor, false)
}

func (u *issueUseCaseImpl) ReopenWisp(ctx context.Context, id string, params ReopenIssueParams, actor string) (ReopenIssueResult, error) {
	return u.reopen(ctx, id, params, actor, true)
}

func (u *issueUseCaseImpl) reopen(ctx context.Context, id string, params ReopenIssueParams, actor string, useWisp bool) (ReopenIssueResult, error) {
	if id == "" {
		return ReopenIssueResult{}, fmt.Errorf("reopen: id must not be empty")
	}
	if actor == "" {
		return ReopenIssueResult{}, fmt.Errorf("reopen: actor must not be empty")
	}
	row, err := u.issueRepo.Reopen(ctx, id, ReopenRowParams{Reason: params.Reason}, actor, IssueTableOpts{UseWispsTable: useWisp})
	if err != nil {
		return ReopenIssueResult{}, fmt.Errorf("reopen %s: %w", id, err)
	}
	issue, err := u.issueRepo.Get(ctx, id, IssueTableOpts{UseWispsTable: row.IsWisp})
	if err != nil {
		return ReopenIssueResult{}, fmt.Errorf("reopen %s: reload: %w", id, err)
	}
	return ReopenIssueResult{
		Issue:    issue,
		Reopened: row.Updated,
	}, nil
}

func (u *issueUseCaseImpl) ClaimIssueIfOpen(ctx context.Context, id, actor string) (ClaimResult, error) {
	return u.claim(ctx, id, actor, false)
}

func (u *issueUseCaseImpl) ClaimWispIfOpen(ctx context.Context, id, actor string) (ClaimResult, error) {
	return u.claim(ctx, id, actor, true)
}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Confirm the issue exists and is currently closed via Get before reopening.
  2. Check the wrapped cause: not-found means bad ID or wrong table routing; fix the ID or wisp flag.
  3. Retry on transient storage errors (locks, connectivity).
  4. If wisp-related, ensure the wisp routing flag matches where the row actually lives.

Example fix

// before
_, err := usecase.ReopenIssue(ctx, id, params, actor)
// after
if _, gerr := usecase.GetIssue(ctx, id); gerr != nil {
    return fmt.Errorf("cannot reopen %s: %w", id, gerr)
}
_, err := usecase.ReopenIssue(ctx, id, params, actor)
Defensive patterns

Strategy: validation

Validate before calling

issue, err := usecase.GetIssue(ctx, id)
if err != nil {
    return fmt.Errorf("cannot reopen %s: %w", id, err)
}
if issue.Status != StatusClosed {
    return fmt.Errorf("%s is not closed", id)
}

Type guard

func isNotFound(err error) bool {
    return errors.Is(err, ErrNotFound)
}

Try / catch

_, err := usecase.ReopenIssue(ctx, id, params, actor)
if err != nil {
    if isNotFound(err) {
        return fmt.Errorf("issue %s not found", id)
    }
    if isTransient(err) {
        return retryReopen(ctx, id, params, actor)
    }
    return err
}

Prevention

When it happens

Trigger: Calling ReopenIssue where issueRepo.Reopen(ctx, id, ReopenRowParams, actor, opts) errors: ID not found in the routed table (main vs wisps per useWisp), database failure, or a driver/constraint rejection.

Common situations: Reopening an already-deleted or mistyped issue ID; the issue is a wisp but the call routed to the main table (or vice versa); transient DB lock/connectivity failure during the update.

Related errors


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