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.