Error Handling Best Practices
Consistent error strategy across libraries vs applications.
Search across all documentation pages
Consistent error strategy across libraries vs applications.
Go teams ship faster when every package agrees on how errors are created, wrapped, inspected, logged, and exposed to clients.
These rules turn the error-handling articles in this section into reviewable habits for libraries, services, and CLIs.
error.error return immediately. Ignored errors are bugs; use linters (errcheck) in CI.(zero, err) on failure; never use the success value when err != nil. Callers depend on this invariant.errors.Is and errors.As instead of == and string matching. Survives wrap chains and wording changes.fmt.Errorf("operation: %w", err) to add context. One wrap per layer with meaningful operation names.nil error on success, not typed nil pointers. Prevents err != nil surprises on success paths.log and return the same error. Avoid duplicate log noise unless adding unique context unavailable upstream.error so callers control exit behavior.%v instead of %w at public boundaries when inner causes must stay hidden. Security and abstraction trade-off.type, gRPC code), not raw Error() text. Clients branch on codes.context.DeadlineExceeded to timeout responses (504/408, gRPC deadline exceeded). Distinguish timeouts from not-found.Retryable() bool or documented gRPC code policy.errors.Join when all fields matter. Single response listing every failure.err variables from goroutines without synchronization. Use channels or errgroup.errgroup.WithContext. Saves quotas and CPU.context.Context and wrap ctx.Err() with %w. Callers distinguish cancel vs I/O failure.debug.Stack in application diagnostics, not in reusable libraries. Stacks are for operators.errors.Is, errors.As, and mapped HTTP/gRPC status. Success-path-only tests miss regressions.ErrX in PascalCase. Matches standard library conventions.Error() strings concise and stable in meaning. Program logic uses fields and Is/As, not substrings.No. Log at boundaries or when adding unique diagnostic context. Otherwise return wrapped errors upstream.
When the error already contains sufficient context for the next caller, or when using %v to intentionally hide the chain.
As few as possible for the documented contract. Prefer extending codes on a type over adding many sentinels.
Yes. Map errors.Is outcomes to exit 1/2/etc. and print user-friendly messages to stderr.
Rarely for user-facing CLI output. Never for library logic; use Is/As.
Shared domain packages stay transport-agnostic; each binary (API, worker, CLI) owns mapping and logging policy.
Prefer stdlib %w, errors.Is, and errors.As in new code. Legacy stacks may wrap gradually.
Enable errcheck, errorlint, and wrapcheck (team policy) in golangci-lint for consistent wrap and compare style.
OpenAPI/gRPC docs listing stable codes and statuses; never document internal driver strings.
When you need all failures collected, not fail-fast. Validation and batch linting are common cases.
Assert errors.Is/As and fields. String equality breaks when messages gain context wraps.
When adding transports (gRPC, GraphQL), changing auth, or after incidents caused by ambiguous status mapping.
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).
Reviewed by Chris St. John·Last updated Jul 16, 2026