gastownhall/beads · error

%w: %s cannot depend on itself

Error message

%w: %s cannot depend on itself

What it means

Returned when a dependency's issue and target are the same ID: an issue cannot depend on itself. This is a deliberate domain validation (wrapped around domain.ErrSelfDependency) so callers can use errors.Is to detect it, raised by CheckDependencyCycleInTx before the edge is inserted.

Source

Thrown at internal/storage/issueops/dependencies.go:443

		  AND s.is_blocked = 0
		  AND s.status <> 'closed' AND s.status <> 'pinned'
		  AND EXISTS (
		    SELECT 1 FROM (
		      SELECT id, status FROM %s WHERE id = ?
		    ) AS t
		    WHERE t.status <> 'closed' AND t.status <> 'pinned'
		  )
	`, sourceTable, targetTable), source, target)
	return err
}

// CheckDependencyCycleInTx rejects self-dependencies and cycles across the
// combined blocks, conditional-blocks, and parent-child graph before insert.
// The caller may pass a restricted depTables list for a known storage bucket;
// nil uses all dependency tables.
func CheckDependencyCycleInTx(ctx context.Context, tx DBTX, dep *types.Dependency, depTables []string) error {
	if dep.IssueID == dep.DependsOnID {
		return fmt.Errorf("%w: %s cannot depend on itself", domain.ErrSelfDependency, dep.IssueID)
	}
	if !types.IsSchedulingEdge(dep.Type) {
		return nil
	}
	wouldCycle, err := WouldCreateSchedulingCycleInTx(ctx, tx, dep.IssueID, dep.DependsOnID, depTables)
	if err != nil {
		return fmt.Errorf("failed to check for dependency cycle: %w", err)
	}
	if wouldCycle {
		return domain.ErrDependencyCycle
	}
	return nil
}

// WouldCreateSchedulingCycleInTx reports whether adding issueID -> dependsOnID
// would close a cycle in the combined scheduling graph. It is shared by the
// classic and domain storage stacks so both traverse the same dependency types
// and typed target columns.

View on GitHub (pinned to 71377f2769)

Solutions

  1. Compare dep.IssueID and dep.DependsOnID before calling and reject equal IDs with a clear client-side message
  2. Fix the caller that is populating both fields with the same value
  3. Use errors.Is(err, domain.ErrSelfDependency) to detect this case and skip the edge instead of failing the batch

Example fix

// before
if err := issueops.CheckDependencyCycleInTx(ctx, tx, dep, nil); err != nil {
	return err
}
// after
if dep.IssueID == dep.DependsOnID {
	return fmt.Errorf("skip self dependency %s", dep.IssueID)
}
if err := issueops.CheckDependencyCycleInTx(ctx, tx, dep, nil); err != nil {
	return err
}
Defensive patterns

Strategy: validation

Validate before calling

func validateNoSelfDep(dep types.Dependency) error {
	if dep.IssueID == dep.DependsOnID {
		return fmt.Errorf("issue %s cannot depend on itself", dep.IssueID)
	}
	return nil
}

Type guard

func isSelfDependency(err error) bool {
	return errors.Is(err, domain.ErrSelfDependency)
}

Try / catch

if err := issueops.CheckDependencyCycleInTx(ctx, tx, dep, nil); err != nil {
	if errors.Is(err, domain.ErrSelfDependency) {
		return ErrSkipSelfDep // handle specifically, don't fail whole batch
	}
	return err
}

Prevention

When it happens

Trigger: Calling CheckDependencyCycleInTx (directly or via addDependencyInTx / PersistDependenciesWithOptionsResult) with a Dependency where IssueID == DependsOnID, e.g. a parent patch assigning an issue as its own parent, or a client echoing back an ID into both fields.

Common situations: Off-by-one or copy-paste bug in CLI/API payloads; syncing tools that map an ID to the wrong field; parent-patch applied to the issue itself.

Related errors


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