docs/ERROR_HANDLING.md
This document describes the error handling patterns used throughout the beads codebase and provides guidelines for when each pattern should be applied.
The beads codebase currently uses three distinct error handling patterns across different scenarios. Understanding when to use each pattern is critical for maintaining consistent behavior and a good user experience.
os.Exit(1))When to use:
Example:
if err := store.CreateIssue(ctx, issue, actor); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
Characteristics:
Error: prefix to stderrFiles using this pattern:
cmd/bd/create.go (lines 31-32, 46-49, 57-58, 74-75, 107-108, etc.)cmd/bd/init.go (lines 77-78, 96-97, 104-105, 112-115, 209-210, 225-227)cmd/bd/sync.go (lines 52-54, 59-60, 82-83, etc.)fmt.Fprintf + continue)When to use:
Example:
if err := createConfigYaml(beadsDir, false); err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to create config.yaml: %v\n", err)
// Non-fatal - continue anyway
}
Characteristics:
Warning: prefix to stderrFiles using this pattern:
cmd/bd/init.go (lines 155-157, 161-163, 167-169, 188-190, 236-238, 272-274, etc.)cmd/bd/sync.go (lines 156, 257, 281, 329, 335, 720-722, 740, 743, 752, 762)cmd/bd/create.go (lines 333-334, 340-341)cmd/bd/sync.go (handles Dolt sync operations)_ = operation())When to use:
Example:
_ = store.Close()
_ = os.Remove(tempPath)
Characteristics:
defer statements or error pathsFiles using this pattern:
cmd/bd/init.go (line 209, 326-327)cmd/bd/sync.go (lines 696-698)cmd/bd/sync.go (sync cleanup)Use this flowchart to choose the appropriate error handling pattern:
┌─────────────────────────────────────┐
│ Did an error occur? │
└─────────────┬───────────────────────┘
│
├─ NO → Continue normally
│
└─ YES → Ask:
│
├─ Is this a fatal error that prevents
│ the command's core purpose?
│
│ YES → Pattern A: Exit with os.Exit(1)
│ • Write "Error: ..." to stderr
│ • Provide actionable hint if possible
│ • Exit code 1
│
├─ Is this an optional/auxiliary operation
│ where the command can still succeed?
│
│ YES → Pattern B: Warn and continue
│ • Write "Warning: ..." to stderr
│ • Explain what failed
│ • Continue execution
│
└─ Is this a cleanup/best-effort operation
where failure doesn't matter?
YES → Pattern C: Silent ignore
• Use _ = operation()
• No user output
• Typically in defer/error paths
priority, err := validation.ValidatePriority(priorityStr)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
if err := createConfigYaml(localBeadsDir, false); err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to create config.yaml: %v\n", err)
// Non-fatal - continue anyway
}
defer func() {
_ = tempFile.Close()
if writeErr != nil {
_ = os.Remove(tempPath)
}
}()
if err := store.SetMetadata(ctx, "last_import_hash", currentHash); err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to update last_import_hash: %v\n", err)
}
if err := store.CreateIssue(ctx, issue, actor); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
// BAD: Same type of operation handled differently
if err := createConfigYaml(dir, false); err != nil {
fmt.Fprintf(os.Stderr, "Warning: %v\n", err) // Warns
}
if err := createReadme(dir); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1) // Exits - inconsistent!
}
// GOOD: Consistent pattern for similar operations
if err := createConfigYaml(dir, false); err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to create config.yaml: %v\n", err)
}
if err := createReadme(dir); err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to create README.md: %v\n", err)
}
// BAD: Critical operation ignored
_ = store.CreateIssue(ctx, issue, actor)
// GOOD: Exit on critical errors
if err := store.CreateIssue(ctx, issue, actor); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
// BAD: Exiting when git hooks fail is too aggressive
if err := installGitHooks(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
// GOOD: Warn and suggest fix
if err := installGitHooks(); err != nil {
yellow := color.New(color.FgYellow).SprintFunc()
fmt.Fprintf(os.Stderr, "\n%s Failed to install git hooks: %v\n", yellow("⚠"), err)
fmt.Fprintf(os.Stderr, "You can try again with: %s\n\n", cyan("bd doctor --fix"))
}
When writing tests for error handling:
os.ExitIMPORTANT: Not all metadata is created equal. There are two distinct categories with different error handling requirements:
Configuration metadata defines fundamental system behavior and must succeed:
// Pattern A: Exit on failure
if err := store.SetConfig(ctx, "issue_prefix", prefix); err != nil {
fmt.Fprintf(os.Stderr, "Error: failed to set issue prefix: %v\n", err)
_ = store.Close()
os.Exit(1)
}
if err := syncbranch.Set(ctx, store, branch); err != nil {
fmt.Fprintf(os.Stderr, "Error: failed to set sync branch: %v\n", err)
_ = store.Close()
os.Exit(1)
}
Examples:
issue_prefix - Defines how all issue IDs are generatedsync.branch - Critical for git synchronization workflowRationale: These settings are prerequisites for basic operation. Without them, the system cannot function correctly. A failure here indicates a serious problem (e.g., filesystem issues, database corruption).
Tracking metadata enhances functionality but the system works without it:
// Pattern B: Warn and continue
if err := store.SetMetadata(ctx, "bd_version", Version); err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to store version metadata: %v\n", err)
// Non-fatal - continue anyway
}
if err := store.SetMetadata(ctx, "repo_id", repoID); err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to set repo_id: %v\n", err)
}
if err := store.SetMetadata(ctx, "last_import_hash", hash); err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to update last_import_hash: %v\n", err)
}
Examples:
bd_version - Enables version mismatch warnings on upgradesrepo_id / clone_id - Helps with collision detection across cloneslast_import_hash - Optimizes staleness detection (falls back to mtime if unavailable)Rationale: System degrades gracefully if tracking metadata is unavailable. Core functionality (creating issues, importing data) still works. Failures here might indicate temporary issues (e.g., read-only filesystem) that shouldn't block the entire operation.
See also: cmd/bd/init.go lines 206-272 for detailed inline documentation of this distinction.
Setting file permissions is typically Pattern B because the file was already written:
if err := os.Chmod(jsonlPath, 0600); err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to set file permissions: %v\n", err)
}
Always use Pattern C for cleanup in error paths:
defer func() {
_ = tempFile.Close() // Pattern C: already handling primary error
if writeErr != nil {
_ = os.Remove(tempPath) // Pattern C: best effort cleanup
}
}()
Consider creating helper functions to enforce consistency:
// FatalError writes error to stderr and exits
func FatalError(format string, args ...interface{}) {
fmt.Fprintf(os.Stderr, "Error: "+format+"\n", args...)
os.Exit(1)
}
// WarnError writes warning to stderr and continues
func WarnError(format string, args ...interface{}) {
fmt.Fprintf(os.Stderr, "Warning: "+format+"\n", args...)
}
cmd/bd/create.go - Examples of Pattern A for user input validationcmd/bd/init.go - Examples of all three patternscmd/bd/sync.go - Examples of Pattern B for metadata operationscmd/bd/sync.go - Examples of Pattern C for cleanup operations