gastownhall/beads · error

element %d is not a string

Error message

element %d is not a string

What it means

decodeWaiters requires every element of the waiters JSON array to be a string. If element i is another JSON type (number, object, bool, null), it returns 'element %d is not a string' with the offending index. Each string is then further validated for UTF-8 via checkUTF8.

Source

Thrown at internal/migration/legacysqlite/reader.go:701

		return fmt.Errorf("legacy SQLite issue waiters serialize to %d bytes (current TEXT maximum %d)", len(waiters), currentTextBytes)
	}
	return nil
}

func decodeWaiters(raw string) ([]string, error) {
	var decoded any
	if err := json.Unmarshal([]byte(raw), &decoded); err != nil {
		return nil, err
	}
	values, ok := decoded.([]any)
	if !ok {
		return nil, fmt.Errorf("must be an array of strings")
	}
	waiters := make([]string, len(values))
	for i, value := range values {
		waiter, ok := value.(string)
		if !ok {
			return nil, fmt.Errorf("element %d is not a string", i)
		}
		if err := checkUTF8(currentString{fmt.Sprintf("waiters[%d]", i), waiter}); err != nil {
			return nil, err
		}
		waiters[i] = waiter
	}
	return waiters, nil
}

func validateIssueVarchars(issue *types.Issue) error {
	fields := []currentVarchar{
		{"issue id", issue.ID, types.MaxFieldLen},
		{"issue title", issue.Title, currentTitleVarcharRunes},
		{"issue status", string(issue.Status), currentShortVarcharRunes},
		{"issue type", string(issue.IssueType), currentShortVarcharRunes},
		{"issue assignee", issue.Assignee, types.MaxFieldLen},
		{"issue created_by", issue.CreatedBy, types.MaxFieldLen},
		{"issue owner", issue.Owner, types.MaxFieldLen},

View on GitHub (pinned to 71377f2769)

Solutions

  1. Convert non-string waiter elements to strings in the legacy row.
  2. Filter out null/non-string entries before migrating.
  3. Fix the legacy exporter to always emit string waiter identifiers.

Example fix

// before
[42, "alice"]
// after
["42", "alice"]
Defensive patterns

Strategy: type-guard

Validate before calling

for i, el := range arr { if _, ok := el.(string); !ok { return fmt.Errorf("waiters[%d] is not a string", i) } }

Type guard

func allStrings(v any) bool { arr, ok := v.([]any); if !ok { return false }; for _, el := range arr { if _, ok := el.(string); !ok { return false } }; return true }

Try / catch

waiters, err := decodeWaiters(raw); if err != nil { log.Warn("bad waiter element, coercing", err); waiters = coerceToStrings(raw) }

Prevention

When it happens

Trigger: A waiters array like '[1, "alice"]' or '[null]' is passed to decodeWaiters; the first non-string element aborts with its index.

Common situations: Waiter entries stored as IDs/numbers in an older legacy format, nulls from partial exports, or hand-edited JSON.

Related errors


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