gastownhall/beads · error

%w: %s is blocked by %v

Error message

%w: %s is blocked by %v

What it means

The close policy refuses to close an issue that is not already closed when the denormalized is_blocked flag is set AND there is at least one live, open direct blocker (blocks/waits-for/conditional-blocks). The error wraps storage.ErrCloseBlocked and lists the blocking issue ids. Force bypasses this refusal (but not the version check). A bare is_blocked=1 with no live direct blocker is deliberately allowed — the live blocker list self-heals stale flags.

Source

Thrown at internal/storage/issueops/close.go:159

	// cell, while a close writes both tiers, so Dolt cannot merge a stale count
	// with a new edge.
	if err := touchDependencyCoordinationInTx(ctx, tx, id); err != nil {
		return 0, err
	}
	openChildren, err := countOpenChildrenForTargetInTx(ctx, tx, id, targetColumn)
	if err != nil {
		return 0, err
	}
	if !force && openChildren > 0 {
		return 0, &storage.CloseOpenChildrenError{IssueID: id, OpenChildren: openChildren}
	}
	if !force && !closed {
		blocked, blockers, err := IsBlockedInTx(ctx, tx, id)
		if err != nil {
			return 0, err
		}
		if blocked && len(blockers) > 0 {
			return 0, fmt.Errorf("%w: %s is blocked by %v", storage.ErrCloseBlocked, id, blockers)
		}
	}
	return openChildren, nil
}

// EnforceClosePolicyInTx applies the close policy to id without closing it, for
// a caller that reaches a done status by some other write. It routes the target
// exactly as a checked close does — probing issues then wisps by ID, preferring
// the durable row when both hold it — rather than deriving the table from an
// is-wisp flag, so a dual-resident ID counts children against the same row the
// close guard would.
//
// It returns the observed open-child count. force bypasses both refusals and
// nothing else; the coordination-cell touch happens either way.
func EnforceClosePolicyInTx(ctx context.Context, tx DBTX, id string, force bool) (int, error) {
	closed, targetColumn, found, err := isClosedInTx(ctx, tx, id)
	if err != nil {
		return 0, err

View on GitHub (pinned to 71377f2769)

Solutions

  1. Close or resolve the listed blocker issues first, then retry the close
  2. Re-run with force=true if the blockers are known-stale or intentionally being overridden
  3. Recompute blocked state (closing a blocker triggers RecomputeIsBlocked) if the blocker list looks wrong
  4. Check dependency direction: a wrongly-added blocks edge can be removed so the target is genuinely unblocked

Example fix

err := store.CloseIssue(ctx, id, reason, actor)
var blocked *storage.CloseBlockedError
if errors.As(err, &blocked) {
	// close blockers first, or override deliberately
	for _, b := range blockers {
		_ = store.CloseIssue(ctx, b, "unblocking "+id, actor)
	}
	err = store.CloseIssue(ctx, id, reason, actor)
}
Defensive patterns

Strategy: validation

Validate before calling

blocked, blockers, err := store.IsBlocked(ctx, id)
if err != nil {
	return err
}
if blocked && len(blockers) > 0 && !force {
	return fmt.Errorf("%s must be unblocked first: %v", id, blockers)
}

Try / catch

err := store.CloseIssue(ctx, id, reason, actor)
var cbErr *storage.CloseBlockedError
if errors.As(err, &cbErr) {
	for _, b := range cbErr.Blockers {
		if err := closeBlocker(ctx, b); err != nil { return err }
	}
	err = store.CloseIssue(ctx, id, reason, actor)
}

Prevention

When it happens

Trigger: bd close (or CloseIssueCheckedInTx / EnforceClosePolicyInTx) without force on an open issue that has at least one open blocker dependency; also triggered for a status change that reaches done via updateIssueInTx through EnforceClosePolicyInTx.

Common situations: Trying to close a parent task whose child still blocks it; workflow automation attempting to close work while dependencies remain open; scripts that close in bulk without ordering by dependency.

Related errors


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