Skip to content

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/if with throw/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 infrausecase boundary.
  • 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