gastownhall/beads · error

%w: %w

Error message

%w: %w

What it means

ClassifyPublicCreateError maps low-level persistence errors from a create batch onto typed public errors. A SQLState 23505 (unique violation) is re-wrapped as storage.ErrAlreadyExists together with the underlying error, so callers can detect duplicates with errors.Is while retaining the root cause.

Source

Thrown at internal/storage/issueops/public_create.go:100

		prepared.WaitsFor.Gate = string(types.WaitsForAllChildren)
	}
	if err := ValidatePublicCreateRequest(prepared); err != nil {
		return publicops.CreateRequest{}, err
	}
	return prepared, nil
}

// ClassifyPublicCreateError adds ErrValidation only to known deterministic
// public-create failures and leaves infrastructure and commit errors intact.
func ClassifyPublicCreateError(err error) error {
	if err == nil || errors.Is(err, storage.ErrValidation) || errors.Is(err, storage.ErrAlreadyExists) {
		return err
	}
	var conflict *domain.DependencyTypeConflictError
	var hierarchyConflict *domain.DependencyHierarchyConflictError
	var stateErr interface{ SQLState() string }
	if errors.As(err, &stateErr) && stateErr.SQLState() == "23505" {
		return fmt.Errorf("%w: %w", storage.ErrAlreadyExists, err)
	}
	if errors.Is(err, storage.ErrPrefixMismatch) || errors.Is(err, domain.ErrSelfDependency) || errors.Is(err, types.ErrFieldTooLong) || errors.Is(err, domain.ErrDependencyCycle) || errors.As(err, &conflict) || errors.As(err, &hierarchyConflict) {
		return publicCreateValidationError(err)
	}
	// A create whose requested relationship names a row that does not exist is
	// refused by the dependency write: as the typed endpoint refusal where the
	// write could name the absent endpoint, and as the target foreign key where
	// it could not. The caller asked for an edge to something absent, so this
	// is a deterministic refusal rather than an infrastructure error: classify
	// it the same way ExecuteCreate refuses a skipped dependency, so every
	// backend reports a missing dependency, parent, or waits-for target as
	// ErrValidation wrapping ErrNotFound.
	var missingEndpoint *domain.DependencyEndpointNotFoundError
	if errors.As(err, &missingEndpoint) || dberrors.IsMissingForeignKeyTarget(err) {
		return publicCreateValidationError(fmt.Errorf("create: dependency target does not exist: %w: %w", err, storage.ErrNotFound))
	}
	return err
}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Handle the duplicate: check errors.Is(err, storage.ErrAlreadyExists) and treat the create as already-done if the existing row matches
  2. Make batch creates idempotent — deduplicate IDs/identities in the batch before calling ExecuteCreateBatch
  3. Unwrap the inner error (%w: %w) to see which constraint/index was violated and adjust the payload

Example fix

// before
_, err := ExecuteCreate(ctx, tx, req)
if err != nil { return err }
// after
_, err := ExecuteCreate(ctx, tx, req)
if err != nil {
    if errors.Is(err, storage.ErrAlreadyExists) {
        return nil // already created; idempotent no-op
    }
    return err
}
Defensive patterns

Strategy: try-catch

Validate before calling

seen := map[string]bool{}
for _, r := range batch {
  if seen[r.Issue.ID] { return errors.New("duplicate id in batch: " + r.Issue.ID) }
  seen[r.Issue.ID] = true
}

Try / catch

_, err := ExecuteCreateBatch(ctx, tx, batch)
if err != nil {
  if errors.Is(err, storage.ErrAlreadyExists) {
    // duplicate: log and continue idempotently
    return nil
  }
  return err
}

Prevention

When it happens

Trigger: Calling ExecuteCreate/ExecuteCreateBatch when the insert violates a unique constraint — most often creating an issue whose ID already exists, or a duplicate row in a batch with identical deterministic identities.

Common situations: Re-running a batch import without idempotency handling; two issues in one batch that normalize to the same identity; retry after a partial failure where the first attempt already committed.

Related errors


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