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
- Filter empty strings out of request.IDs before the call (slices.DeleteFunc or a loop).
- Fix the input producer: use strings.Fields or skip empty segments when splitting.
- Trim whitespace first so ids like " " are caught and dropped rather than passing an invalid-but-nonempty value.
- 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
- Filter/trim IDs before building EdgeReadRequest
- Use strings.Fields for whitespace-delimited input
- Validate at the boundary where user input enters the program
- Reuse a shared ID-normalization helper across read and count paths
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
- db: Exists: id must not be empty
- list dep metadata: sourceID must not be empty
- iter dep metadata: sourceID must not be empty
- count by id: sourceID must not be empty
- reopen: id must not be empty
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/a7fa98822631e268.
Report an issue: GitHub.