Skip to content

HTTP & CLI Boundaries

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.

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)

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.",
},
},
})

Messages resolve in this order (first match wins):

  1. HandleConfig.TemplateOverride[code] — per-call override
  2. RegisterTemplate(code, tmpl) — global registry
  3. Built-in defaultMessages[code] — exact-match defaults
  4. Family.DefaultMessage() — generic fallback

All lookups are exact code matches (case-insensitive). Template placeholders use {key} syntax.

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))

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."
}
Family HTTP Status
Rejection 400
Conflict 409
Transient 503
Corruption 500
Infrastructure 503

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(),
})
}
}

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
}