Error Handling Guidelines¶
Applies regardless of whether the language uses exceptions (Java, C++) or error values (Go).
- Never swallow errors — always propagate or handle explicitly.
- Add context when wrapping: what operation failed and on what input.
- Distinguish recoverable errors (propagate) from unrecoverable ones (abort/panic/terminate).
- Use typed or structured errors where the language permits.
- Give a typed/structured error its data as separate, directly accessible fields — not only baked
into the formatted message/
toString()/Error()/what(). This lets every consumer (structured logger, CLI stderr/--output json, API error response) render or use the error appropriately for its own channel, without parsing text. Applies even without a structured-logging backend — a CLI tool still benefits from a discriminable error object for its own output formatting and exit-code selection. - Never use exceptions to implement branching logic within a function (e.g., replacing
break/ifwiththrow/catch). Signaling an expected outcome (e.g., "not found") via a typed/domain exception that propagates unmodified to a single boundary handler (see below) is not control flow — it is the exception-language equivalent of a typed return value. In Go, prefer explicit(T, error)returns for the same cases, since Go has no idiomatic typed-exception mechanism. - Handle errors at boundaries only (API handlers, message consumers, top-level entry point) — not mid-stack.
- Translate low-level errors to domain errors at the
infra→usecaseboundary. - Log at the point of handling — never at intermediate layers; never log and re-throw (causes duplicate log entries).
- Programmer errors (broken invariants) → abort/panic. Operational errors (timeouts, I/O) → propagate and handle.
Directives¶
- Never swallow errors; always propagate or handle explicitly
- Wrap errors with context: what operation failed and on what input
- A typed/structured error exposes its data as separate fields, not only inside the formatted
message/
toString()/Error()/what()— every consumer (logger, CLI output, API response) renders it independently, without parsing text; this applies even without structured logging - Use typed/structured errors; never use exceptions for branching logic within a function — a typed exception propagated unmodified to a single boundary handler (e.g., "not found" surfacing as a 404) is not control flow and is fine in exception-based languages; Go uses explicit
(T, error)returns instead, since it has no idiomatic typed-exception mechanism - Handle errors at boundaries only (API handlers, consumers, entry point) — not mid-stack
- Translate low-level errors to domain errors at the infra→usecase boundary
- Log at the point of handling only — never log and re-throw (causes duplicate log entries)
- Programmer errors (broken invariants) → abort/panic; operational errors (timeouts, I/O) → propagate