gastownhall/beads · error · storage.ErrValidation

%w: read edges id %d is empty

Error message

%w: read edges id %d is empty

What it means

ValidateEdgeReadRequest rejects an EdgeReadRequest whose IDs slice contains an empty string at position %d. An empty ID names no issue, so the library refuses rather than returning a misleading per-anchor entry. It wraps storage.ErrValidation; an entirely empty IDs slice is valid and returns an empty result.

Source

Thrown at internal/storage/issueops/edges.go:22

	"context"
	"fmt"
	"sort"
	"strings"

	"github.com/steveyegge/beads/internal/storage"
	"github.com/steveyegge/beads/internal/types"
	publicops "github.com/steveyegge/beads/issueops"
)

// ValidateEdgeReadRequest applies the request rules every EdgeReader
// implementation shares. Both tell a caller's mistake from a legitimately empty
// answer: an empty ID entry names nothing, and an unusable dependency type would
// become a filter that silently matches nothing. An empty ID SLICE is neither —
// it asks about no anchors and gets none back.
func ValidateEdgeReadRequest(request publicops.EdgeReadRequest) error {
	for i, id := range request.IDs {
		if id == "" {
			return fmt.Errorf("%w: read edges id %d is empty", storage.ErrValidation, i)
		}
	}
	for i, depType := range request.Types {
		if !depType.IsValid() {
			return fmt.Errorf("%w: read edges type %d is not a usable dependency type (non-empty, max %d chars)",
				storage.ErrValidation, i, types.MaxDependencyTypeLen)
		}
	}
	return nil
}

// EdgeReadAnchors is the de-duplicated anchor list a read runs against: the
// request's ids with repeats collapsed onto their first mention.
//
// It is shared rather than a loop in each implementation because the
// de-duplication decides the SHAPE of the answer — one entry per distinct id, in
// first-mention order. BlockingAnnotator makes the same promise over the same
// shape of request (blocking_annotation.go) and reaches it here too.

View on GitHub (pinned to 71377f2769)

Solutions

  1. Filter empty strings out of request.IDs before the call (slices.DeleteFunc or a loop).
  2. Fix the input producer: use strings.Fields or skip empty segments when splitting.
  3. Trim whitespace first so ids like " " are caught and dropped rather than passing an invalid-but-nonempty value.
  4. Validate IDs at the CLI/API boundary and reject blank arguments with a clear message.

Example fix

// before
req := publicops.EdgeReadRequest{IDs: strings.Split(list, ",")}
// after
var ids []string
for _, s := range strings.Split(list, ",") {
	if s = strings.TrimSpace(s); s != "" { ids = append(ids, s) }
}
req := publicops.EdgeReadRequest{IDs: ids}
Defensive patterns

Strategy: validation

Validate before calling

func hasEmptyID(ids []string) bool { return slices.Contains(ids, "") }

Type guard

func nonEmptyIDs(ids []string) []string {
	return slices.DeleteFunc(slices.Clone(ids), func(s string) bool { return s == "" })
}

Try / catch

if err := ValidateEdgeReadRequest(req); err != nil {
	if errors.Is(err, storage.ErrValidation) && strings.Contains(err.Error(), "is empty") {
		req.IDs = nonEmptyIDs(req.IDs)
	}
}

Prevention

When it happens

Trigger: Calling ReadEdges / ExecuteEdgeRead with EdgeReadRequest{IDs: []string{"bd-1", ""}}; building IDs from strings.Split on a comma list with blank or trailing elements; deserializing a JSON array containing "".

Common situations: Shell scripts passing untrimmed arguments; a UI sending a blank row from an editable list; generators that append empty IDs when an optional reference is unset.

Related errors


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