HTTP & CLI Boundaries
CLI Boundary: HandleError
Section titled “CLI Boundary: HandleError”HandleError is the top-of-main handler that classifies the error, picks an exit code, formats a user-facing message, and writes to stderr:
func main() { if err := run(); err != nil { os.Exit(errorfamily.HandleError(err)) }}The output follows a structured format: What / Why / Fix / WayOut.
Structured Result (No stderr Write)
Section titled “Structured Result (No stderr Write)”For HTTP handlers, gRPC interceptors, or programmatic use:
result := errorfamily.HandleErrorDetailed(err)fmt.Printf("exit=%d msg=%q fix=%q\n", result.ExitCode, result.Message, result.SuggestedFix)Context-Propagating Handler
Section titled “Context-Propagating Handler”The canonical entry point when you have a context.Context:
exitCode := errorfamily.HandleErrorWithContext(ctx, err, errorfamily.HandleConfig{ DiagnosticFunc: myDiagnoseFunc, Output: myWriter, TemplateOverride: map[string]errorfamily.MessageTemplate{ "file.not_found": { What: "Could not find {path}", Why: "The file doesn't exist at the expected location.", Fix: "Check that {path} exists and is readable.", WayOut: "Run with --verbose for more details.", }, },})Template Resolution
Section titled “Template Resolution”Messages resolve in this order (first match wins):
HandleConfig.TemplateOverride[code]— per-call overrideRegisterTemplate(code, tmpl)— global registry- Built-in
defaultMessages[code]— exact-match defaults Family.DefaultMessage()— generic fallback
All lookups are exact code matches (case-insensitive). Template placeholders use {key} syntax.
HTTP Boundary
Section titled “HTTP Boundary”HTTPStatus(err) maps any error to its status code. HTTPHandler wraps an error-returning handler and writes a safe JSON response (no internal leakage):
func createOrder(w http.ResponseWriter, r *http.Request) error { order, err := parseOrder(r) if err != nil { return errorfamily.WrapRejectionf(err, "order.parse", "invalid field") } if err := repo.Save(order); err != nil { return errorfamily.WrapConflict(err, "order.duplicate", "order already exists") } json.NewEncoder(w).Encode(order) return nil}
mux.Handle("/api/orders", errorfamily.HTTPHandler(createOrder))Safe JSON Responses
Section titled “Safe JSON Responses”HTTPHandler never leaks err.Error(). The response body contains only family, code, and a user-facing message from a registered template:
{ "family": "conflict", "code": "order.duplicate", "message": "A conflict was detected."}Status Code Mapping
Section titled “Status Code Mapping”| Family | HTTP Status |
|---|---|
| Rejection | 400 |
| Conflict | 409 |
| Transient | 503 |
| Corruption | 500 |
| Infrastructure | 503 |
Custom Response Shape
Section titled “Custom Response Shape”For a custom response, write your own response and use HTTPStatus(err) directly:
func handler(w http.ResponseWriter, r *http.Request) { err := doWork(r) if err != nil { status := errorfamily.HTTPStatus(err) w.WriteHeader(status) json.NewEncoder(w).Encode(map[string]any{ "error": errorfamily.Code(err), "type": errorfamily.Classify(err).String(), }) }}Structured Logging
Section titled “Structured Logging”LogError logs classified fields at Warn for Transient and Error for everything else:
if err := run(); err != nil { errorfamily.LogError(err, slog.Default()) // → level=WARN msg="db timeout" family=transient code=db.timeout retryable=true}