Docs Error Propagation

Error Propagation

Errors are signals.

They describe:

  • What went wrong
  • Where it went wrong
  • Whether recovery is possible
  • Who should react

In many systems, errors lose meaning as they travel upward —
wrapped inconsistently, logged repeatedly, or converted too early.

Plumego encourages a clear principle:

Errors should travel upward with their intent intact, and be translated only at boundaries.

This document defines a pattern for error propagation that preserves clarity across layers.


The Core Problem with Error Handling

Common error-handling pathologies include:

  • Errors turned into HTTP responses too early
  • Transport-specific errors leaking into core logic
  • Errors logged at every layer
  • String-based error comparisons
  • Loss of semantic meaning during wrapping

The result is fragile code and inconsistent behavior.


Error Propagation vs Error Handling

A critical distinction:

  • Error propagation: moving an error upward unchanged
  • Error handling: deciding what to do with the error

Most layers should propagate, not handle.

Handling belongs at explicit decision points.


Errors Are Part of the Contract

An error is not just a failure.

It is part of the API contract between layers.

When a usecase returns an error, it is making a statement:

“This operation could not be completed for a specific reason.”

Preserving that reason matters.


Layered Error Responsibilities

In a Plumego system, error responsibilities are divided deliberately.

Layer Responsibility
Domain Define invariant violations
Usecase Define operation-level failures
Handler Translate errors to HTTP
Middleware Handle cross-cutting failures

No layer should assume another layer’s job.


Domain Errors: Pure and Intentional

Domain errors represent violations of business invariants.

Characteristics:

  • Defined as sentinel errors or typed errors
  • Independent of transport
  • Stable over time

Example:

var ErrCannotCancel = errors.New("order cannot be cancelled")

Domain code returns errors —
it does not interpret them.


Usecase Errors: Contextual but Transport-Agnostic

Usecase errors represent failure to complete an operation.

They may:

  • Wrap domain errors
  • Add contextual meaning
  • Represent authorization or workflow failures

Example:

if !u.permission.Allowed(userID) {
	return ErrForbidden
}

Usecases should not return HTTP status codes.


Error Wrapping: Use with Care

Go’s error wrapping is powerful, but easy to abuse.

Guidelines:

  • Wrap errors to add context
  • Do not wrap to hide intent
  • Preserve the original error for errors.Is

Example:

return fmt.Errorf("save order failed: %w", err)

Avoid wrapping with strings that replace meaning.


Propagate First, Translate Later

The central rule:

Do not translate errors until you reach a boundary.

Boundaries include:

  • HTTP handlers
  • RPC adapters
  • CLI commands
  • Background job runners

Inside the core, errors remain semantic, not representational.


Handler-Level Error Translation

Handlers are responsible for translating errors into HTTP responses.

Example:

err := usecase.Execute(input)
if err != nil {
	switch {
	case errors.Is(err, ErrForbidden):
		ctx.JSON(http.StatusForbidden, errorResponse(err))
	case errors.Is(err, ErrNotFound):
		ctx.JSON(http.StatusNotFound, errorResponse(err))
	default:
		ctx.JSON(http.StatusInternalServerError, errorResponse("internal error"))
	}
	return
}

This keeps translation:

  • Centralized
  • Explicit
  • Easy to audit

Avoid Logging Errors Multiple Times

A common mistake is logging the same error at every layer.

This leads to:

  • Noisy logs
  • Duplicated stack traces
  • Confusing timelines

Recommended approach:

  • Log errors once, at the boundary
  • Attach context (trace ID, request info)
  • Propagate errors silently elsewhere

Errors vs Panics

Errors represent expected failure modes.
Panics represent programmer errors.

Do not use panics for control flow.

Panic recovery is a safety net, not an error strategy.


Typed Errors vs Sentinel Errors

Both approaches are valid.

Sentinel errors

  • Simple
  • Easy to compare
  • Stable

Typed errors

  • Carry structured data
  • Useful for complex cases
  • Require more discipline

Choose one strategy consistently per domain.


Avoid String-Based Error Logic

Never write code like:

if err.Error() == "not allowed" { ... }

This is brittle and unmaintainable.

Always use errors.Is or type assertions.


Error Propagation and Testing

Clear error propagation simplifies tests:

  • Domain tests assert specific errors
  • Usecase tests assert semantic failures
  • Handler tests assert HTTP translation

Each layer is tested for its responsibility only.


Common Anti-Patterns

Translating Errors Too Early

This couples core logic to transport concerns.


Swallowing Errors

Errors should not disappear silently.


Logging and Returning Different Errors

This creates confusion and inconsistency.


Summary

In Plumego:

  • Errors are semantic signals
  • Most layers propagate, not handle
  • Translation happens at boundaries
  • Logging is centralized
  • Intent is preserved

Clear error propagation keeps systems understandable under failure —
which is when understanding matters most.


Next

With error propagation clarified, the next pattern is:

Boundary Translation

This formalizes how data and errors cross system boundaries without leaking concerns.