gastownhall/beads · info
failed to marshal backup state: %w
Error message
failed to marshal backup state: %w
What it means
saveBackupState wraps json.MarshalIndent failures when serializing the backupState struct before writing backup_state.json. Marshal of a plain struct of strings/times should never fail in practice; this is a defensive wrapper around an effectively impossible error path.
Source
Thrown at cmd/bd/backup_export.go:77
data, err := os.ReadFile(path) //nolint:gosec // path is constructed internally
if os.IsNotExist(err) {
return &backupState{}, nil
}
if err != nil {
return nil, fmt.Errorf("failed to read backup state: %w", err)
}
var state backupState
if err := json.Unmarshal(data, &state); err != nil {
return nil, fmt.Errorf("failed to parse backup state: %w", err)
}
return &state, nil
}
// saveBackupState writes the backup state file atomically.
func saveBackupState(dir string, state *backupState) error {
data, err := json.MarshalIndent(state, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal backup state: %w", err)
}
return atomicWriteFile(filepath.Join(dir, "backup_state.json"), data)
}
// atomicWriteFile writes data to a same-directory temp file, fsyncs the
// temp file's own contents, then renames it into place. This avoids a
// truncated/partial file at path if the process crashes mid-write.
//
// Two caveats this does NOT cover, narrowing the "crash-safe" claim rather
// than the implementation (existing callers' behavior is unchanged here):
// - Only the temp file's contents are fsynced, not the parent directory
// entry; a crash between the rename and a subsequent directory fsync
// can still lose the rename itself on some filesystems.
// - os.Rename's atomic-replace guarantee is a POSIX/Unix property; it is
// not guaranteed on Windows. It also does not follow a symlink at
// path — it replaces whatever is there, symlink or not — so a caller
// that must preserve a symlink's target should resolve path with
// filepath.EvalSymlinks first (see cmd/bd/proxied_server.go).View on GitHub (pinned to 71377f2769)
Solutions
- Re-run the command; transient state may resolve it.
- If reproducible, it indicates a bug in the backupState struct definition — check recently added fields and file an issue with the bd version.
- Update bd to the latest version in case the struct regression was already fixed.
Defensive patterns
Strategy: retry
Validate before calling
// backupState is a plain struct of strings/times; nothing to pre-validate. // Marshal failure implies a build/struct bug — verify bd version: // bd --version
Try / catch
if err := saveBackupState(dir, state); err != nil {
if strings.Contains(err.Error(), "marshal backup state") {
log.Printf("bug: unmarshalable backupState (bd version %s): %v", version, err)
}
return err
} Prevention
- Keep bd updated to the latest release
- If this fires, it is a code bug — report it with bd --version and reproduction steps
When it happens
Trigger: json.MarshalIndent(state) returns non-nil — theoretically only if backupState contained an unsupported type (chan, func, invalid time.Time from corrupt in-memory state).
Common situations: Essentially never seen in production; backupState holds only string and time fields. Would indicate a code change introduced an unmarshalable field.
Related errors
- failed to marshal issue %s: %w
- failed to parse backup state: %w
- failed to marshal interactions log entry: %w
- marshal create request: %w
- serializing waits-for metadata: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/9e1e01db12184d83.
Report an issue: GitHub.