gastownhall/beads · error
journal: marshal issue %s: %w
Error message
journal: marshal issue %s: %w
What it means
insertEventRow marshals the issue snapshot to JSON before inserting the journal row; if json.Marshal fails the error is wrapped as 'journal: marshal issue'. Since types.Issue is JSON-tag-annotated this should be nearly impossible, but it surfaces invalid field types (e.g. non-UTF8, unsupported types injected via custom fields).
Source
Thrown at internal/storage/issueops/journal.go:425
}
issue.IsBlocked = blocked != 0
return issue, nil
}
return nil, fmt.Errorf("%w: issue %s", storage.ErrNotFound, issueID)
}
// insertEventRow performs the actual INSERT. It is the ONE seam both write
// plumbings funnel through, so the seq mechanism cannot drift between them. A
// nil issue is stored as SQL NULL (deletes); a nil dep is stored as SQL NULL
// (non-dependency ops). ts is the insert time, stamped inside the committing
// transaction. actor is stored as-is — "" for the genuinely unattributable
// paths — in the NOT NULL DEFAULT ” actor column.
func insertEventRow(ctx context.Context, tx DBTX, op EventOp, issueID string, issue *types.Issue, dep *EventDep, comment *EventComment, actor string) error {
var issueJSON any
if issue != nil {
b, err := json.Marshal(issue)
if err != nil {
return fmt.Errorf("journal: marshal issue %s: %w", issueID, err)
}
issueJSON = string(b)
}
var depJSON any
if dep != nil {
b, err := json.Marshal(dep)
if err != nil {
return fmt.Errorf("journal: marshal dep for %s: %w", issueID, err)
}
depJSON = string(b)
}
var commentJSON any
if comment != nil {
b, err := json.Marshal(comment)
if err != nil {
return fmt.Errorf("journal: marshal comment for %s: %w", issueID, err)
}
commentJSON = string(b)View on GitHub (pinned to 71377f2769)
Solutions
- Inspect the wrapped %w error to find the offending field
- Sanitize issue metadata to JSON-serializable types before calling journal APIs
- Upgrade/downgrade so the in-process types.Issue matches the persisted data
- Report upstream if stock types.Issue fails to marshal (likely a bug)
Example fix
// before
issue.Metadata["cb"] = func() {} // unmarshalable
ops.RecordEventInTx(ctx, tx, op, id, issue, nil, nil)
// after
issue.Metadata["cb"] = nil // strip unsupported values before journaling
ops.RecordEventInTx(ctx, tx, op, id, issue, nil, nil) Defensive patterns
Strategy: try-catch
Validate before calling
if _, err := json.Marshal(issue); err != nil {
return fmt.Errorf("issue %s not JSON-serializable: %w", issue.ID, err)
} Try / catch
if err := ops.RecordEventInTx(ctx, tx, op, id, issue, nil, nil); err != nil {
if strings.Contains(err.Error(), "marshal issue") {
return fmt.Errorf("bad issue payload: %w", err)
}
return err
} Prevention
- Keep issue metadata to JSON-native types (string, number, bool, map, slice)
- Test-marshall payloads in importers before writing
- Avoid injecting functions/cycles into types.Issue fields
When it happens
Trigger: RecordEventInTx/RecordDeleteInTx/RecordDepEventInTx/RecordCommentEventInTx called with an *types.Issue whose contents cannot be marshaled — typically custom metadata holding unmarshalable values or corrupted in-memory state.
Common situations: Custom importers stuffing raw map/func values into issue metadata structs; version mismatch where a field type changed and holds incompatible data; fixtures with cyclic references.
Related errors
- journal: marshal dep for %s: %w
- journal: marshal comment for %s: %w
- failed to marshal backup state: %w
- failed to marshal issue %s: %w
- failed to write JSON: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/e927c23435dd6c91.
Report an issue: GitHub.