gastownhall/beads · error

%s must be a string, got %T

Error message

%s must be a string, got %T

What it means

The value provided for the append-notes operation is not a string. OpAppendNotes appends text to the issue's existing notes, so its value must be a Go string; any other type is rejected with this type-error message that reports the actual type received.

Source

Thrown at internal/storage/issueops/update.go:975

		return nil, fmt.Errorf("failed to marshal metadata: %w", err)
	}
	return json.RawMessage(result), nil
}

// resolveNotesAppendOp folds OpAppendNotes into a concrete "notes" value on
// resolved, appending to oldIssue.Notes (read in the same mutation transaction).
// It is a no-op when the append op is absent.
func resolveNotesAppendOp(oldIssue *types.Issue, updates, resolved map[string]interface{}) error {
	raw, ok := updates[OpAppendNotes]
	if !ok {
		return nil
	}
	if _, direct := resolved["notes"]; direct {
		return fmt.Errorf("%w: cannot combine a notes replacement with %s", storage.ErrValidation, OpAppendNotes)
	}
	text, ok := raw.(string)
	if !ok {
		return fmt.Errorf("%s must be a string, got %T", OpAppendNotes, raw)
	}
	combined := oldIssue.Notes
	if combined != "" {
		combined += "\n"
	}
	combined += text
	resolved["notes"] = combined
	return nil
}

// mergeOpStrings coerces a merge-operation value to []string. Accepts
// []interface{} of strings as well, so operation maps survive a JSON
// round-trip (e.g. daemon transports).
func mergeOpStrings(op string, value interface{}, present bool) ([]string, error) {
	if !present {
		return nil, nil
	}
	switch v := value.(type) {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Convert the value to a string before inserting: fmt.Sprintf, strconv, or explicit dereference of *string
  2. Use a typed update builder that enforces string for append-notes
  3. Check the reported %T in the message to see which type actually arrived

Example fix

// before
var note *string = getNote()
updates[issueops.OpAppendNotes] = note // *string, not string

// after
if note != nil {
    updates[issueops.OpAppendNotes] = *note // plain string
}
Defensive patterns

Strategy: type-guard

Validate before calling

if v, ok := updates[issueops.OpAppendNotes]; ok {
    if _, isStr := v.(string); !isStr {
        return fmt.Errorf("OpAppendNotes must be a string, got %T", v)
    }
}

Type guard

func isString(v any) bool { _, ok := v.(string); return ok }

Try / catch

if err := issueops.ResolveMergeOps(issue, updates, resolved); err != nil {
    if strings.Contains(err.Error(), "must be a string, got") {
        return fmt.Errorf("coerce the append-notes value to string before retrying: %w", err)
    }
}

Prevention

When it happens

Trigger: Passing a non-string (int, []string, fmt.Stringer, *string, etc.) as the OpAppendNotes value in an update map passed to ResolveMergeOps.

Common situations: Building updates with a generic map[string]interface{} where a helper inserts the wrong type, passing a *string instead of dereferencing it, or deserializing a JSON number/bool into the append field.

Understand the failure class

Background: "Wrong argument type", "must be a string", "expected Array or Prism::Scope": TypeError and ArgumentError when a library receives a value of the wrong type — this error's family across 28 libraries.

Related errors


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