Clean code in Go · 3 MIN READ
Errors: preserve cause and choose one reporting boundary
Keep failure context useful without logging the same failure at every layer.
Keep failure context useful without logging the same failure at every layer. An error value travels to the layer that can decide what to do: retry, map to a response or stop. Trace the worked Go example, then compare the benefit with the cost before applying this design to your own project.
Clean Go · A MAP OF THE IDEA
- Dependency returns a cause
- Application adds operation context
- Boundary classifies and reports once
Useful when
- Callers need failure classification
- Operations need useful diagnostic context
Watch out
- Wrapping can expose implementation contracts
- Sensitive fields do not belong in errors
Implementation & operation cost: Low
The design decision
An error value travels to the layer that can decide what to do: retry, map to a response or stop. Wrap a cause when callers need classification. Return enough context to locate the operation, but avoid adding secrets or logging the same failure as it moves upward.
A complete Go example
Read this program from the entry point and follow the calls shown in the diagram. The output is included at the end so you can check the behavior without a compiler.
package main
import (
"errors"
"fmt"
)
var ErrMissing = errors.New("missing project")
func Load(id string) error { return fmt.Errorf("load %s: %w", id, ErrMissing) }
func main() {
err := Load("p1")
fmt.Println(errors.Is(err, ErrMissing))
fmt.Println(err)
}
// Output:
// true
// load p1: missing project
Wrapping preserves ErrMissing for errors.Is while adding the project operation for a human. The program prints once at the boundary. Replacing %w with %v would keep the message but lose this cause chain.
Tradeoffs and the simpler alternative
Expose stable error categories only when they are part of the package contract. Implementation details can become accidental API commitments when wrapped. Panic is appropriate for violated programmer assumptions in some APIs, not an ordinary missing record or malformed request.
Classify without parsing message text
Use errors.Is for stable sentinel categories and errors.As for documented structured error types. Messages can add operation context for a human but should not become machine parsing protocols. Wrap with %w only when exposing the cause is part of the API contract. Otherwise map to an application category and retain safe diagnostic context. A database missing-row error should not automatically become a permanent public dependency on the database driver.
switch {
case errors.Is(err, ErrMissing):
// Map the documented absence contract at the transport boundary.
case errors.Is(err, context.DeadlineExceeded):
// Distinguish timeout from invalid input; retry only if safe.
case err != nil:
// Report a safe diagnostic once, with an operation identifier.
}This is a focused layout or boundary fragment, not a second standalone program. Read it together with the complete worked example above.
Choose the layer that can recover
The layer knowing an operation is idempotent may choose a bounded retry; a low-level helper should not retry every failure blindly. A transport maps validation and authorization errors to appropriate responses without leaking SQL or credentials. Log once where the error is finally handled, while intermediate layers return useful context. An error can accompany useful partial output, so inspect the API contract before discarding data or returning early.
Test cancellation and the public error promise
Assert errors.Is or errors.As for categories that callers are allowed to depend on, and check state was not partially committed after failure. Test a wrapped cancellation and a typed-nil implementation. Avoid comparing whole human messages unless their exact text is a deliberate contract. Panic and recover are not a substitute for expected validation errors. Do not recover a panic and silently return success; recover only at a defined boundary with a clear failed-operation result.
TAKE THIS WITH YOU
Adds context while preserving a cause for errors.Is. Expose stable error categories only when they are part of the package contract. Implementation details can become accidental API commitments when wrapped. Panic is appropriate for violated programmer assumptions in some APIs, not an ordinary missing record or malformed request.
Check your understanding
Which statement best describes Errors: preserve cause and choose one reporting boundary?
Go to the source
Progress stays in this browser. No account needed.