ADR-000: Uniform Error Handling
ADR-000: Uniform Error Handling Across DocBuilder
Date: 2025-10-03
Updated: 2025-12-14
Status
✅ Implemented - Consolidated error systems completed December 2025
Context
DocBuilder currently mixes error patterns: direct fmt.Errorf/errors.New, partial use of foundation.Result, and ad-hoc wrapping. This causes inconsistent user messages, logging, and exit/HTTP codes.
Decision
Adopt a single error model based on internal/errors with:
- Category, severity, code, operation (op), cause, context fields, retry-eligible flag
- Helper constructors/wrappers:
New,Wrap, and option setters:WithCode,WithOp,WithField,WithRetryable,WithHTTPStatus,WithExitCode - Boundary adapters for CLI/HTTP and standardized logging fields
- CI guard to prevent raw error creation in non-test code
Taxonomy
- Categories: Config, Auth, Git, Docs, Hugo, Pipeline, State, Daemon, Network, IO, Validation, NotFound, Conflict, Timeout, Canceled, RateLimit, Unknown
- Severity: Info, Warning, Error, Fatal
- Codes (examples):
ConfigNotFound,ConfigInvalidYAML,ConfigValidationFailed,GitAuthFailed,GitFetchFailed,DocsWalkFailed,FrontMatterInvalid,PlanCircularDependency,StateReadFailed,StateWriteFailed,ScheduleInvalid
Layer Behavior
- Libraries (config/git/docs/Hugo/pipeline/state): return typed errors, include
op,code, retry-eligible, attach context fields; no logging - Services: aggregate/wrap with higher-level
opand identifiers (repo/path/url) - CLI: map errors → exit codes with
ExitCodeFor(err)and format user-facing messages withFormatForUser(err) - HTTP: map errors → HTTP status with
HTTPStatusFor(err), return JSON{ error: { code, category, message, correlationId }, details? } - Logging (boundary only): structured fields from error (category, code, op, retry-eligible, identifiers), level from severity
Mapping Rules
- CLI exit codes: 0 OK; 2 Validation/Config; 10 Auth; 11 Git; 12 Docs; 13 Hugo; 20 Network (retry-eligible); 1 default
- HTTP status: 400 Validation; 401/403 Auth; 404 NotFound; 409 Conflict; 429 RateLimit; 504 Timeout; 500 Unknown/Internal; 503 service unavailable
Migration Plan
- Harden
internal/errorsAPI (options/extractors for Code/Op/Retryable/HTTP/Exit) - Add adapters:
internal/cli/error_adapter.go(ExitCodeFor, FormatForUser)internal/daemon/http_error_adapter.go(HTTPStatusFor, JSON response builder)internal/logx/log_err.go(structured logging helper)
- Refactor batch 1:
internal/config,internal/git,internal/docs,internal/hugo,internal/pipeline - Refactor batch 2:
internal/daemon,internal/state - Align
foundation.Result[T]usages to carry typedinternal/errorserrors - Tests: mapping tests, adapter tests, update assertions to check category/code
- CI enforcement: script to fail on raw
fmt.Errorf/errors.Newoutside tests andinternal/errors - Documentation: this ADR + CONTRIBUTING note
Edge Cases
- context.Canceled → Category Canceled, Info, not retry-eligible; HTTP 499/408; CLI non-zero depending on command
- context.DeadlineExceeded → Timeout, retry-eligible; HTTP 504; CLI 20
- Multi-errors: wrap
errors.Joinonce, keep causes for Is/As - Preserve
errors.Is/Asby retaining root cause
Examples
Before: return fmt.Errorf("failed to read config file: %w", err)
After: return errors.Wrap(err, errors.CategoryConfig, errors.SeverityError, "read config file", errors.WithOp("config.Load"), errors.WithCode(errors.ConfigReadFailed))
CLI: os.Exit(clierrors.ExitCodeFor(err))
HTTP: status := httperrors.HTTPStatusFor(err) and return JSON problem response
Consequences
- Pros: consistent UX/telemetry, easier support, better retry and policy decisions
- Cons: initial refactor effort, small learning curve
Rollout
- Day 1: error API + adapters + tests
- Day 2–3: batch 1 refactor + tests
- Day 4–5: batch 2 refactor + tests
- Enable CI guard; iterate on any stragglers
Implementation Notes (December 2025)
The error system was successfully consolidated using internal/foundation/errors/ as the single source of truth:
What Was Implemented:
- ✅ Type-safe
ErrorCategoryenum (replaces string-basedErrorCode) - ✅ Fluent builder API with
WithContext(),WithSeverity(),WithRetry() - ✅ HTTP adapter (
internal/foundation/errors/http_adapter.go) - ✅ CLI adapter (
internal/foundation/errors/cli_adapter.go) - ✅ Retry semantics built into error classification
- ✅ Structured context via
ErrorContextmap - ✅ Convenience constructors:
ValidationError(),NotFoundError(), etc.
Migration Completed:
- ✅ Removed duplicate
internal/foundation/errors.go(240 lines) - ✅ Migrated
internal/state/package (12 files) - ✅ Migrated
internal/services/package (2 files) - ✅ Migrated
internal/config/package (4 files) - ✅ Updated all tests to use new API
- ✅ All 43 packages passing tests
- ✅ Zero linting issues
Key Pattern Changes:
foundation.ValidationError()→errors.ValidationError()foundation.ErrorCodeValidation→errors.CategoryValidationclassified.Code→classified.Category()WithContext(Fields{"k": v})→WithContext("k", v)(chained)AsClassified(err, &c) bool→c, ok := AsClassified(err)
Progress Checklist
- Finalize
internal/foundation/errorsAPI (constructors, options, extractors) - Implement CLI adapter (
internal/foundation/errors/cli_adapter.go) - Implement HTTP adapter (
internal/foundation/errors/http_adapter.go) - Refactor batch 1: config, state, services
- Update tests to assert category and add mapping tests
- Wire adapters into
cmd/docbuilder/main.go(future work) - Update daemon HTTP handlers to use adapter (future work)
- Add CI guard for raw error creation (future work)
- Update documentation (STYLE_GUIDE.md, copilot-instructions.md)