The Go error pattern I actually use (part 5)
Five years of writing Go for production. Here's the error-handling shape that survived contact with on-call.
1 min readby Site Admin
What I do#
Three rules:
- Wrap at boundaries. Every error crossing a layer (handler → service → repo) gets a
fmt.Errorf("<verb>: %w", err). - Sentinel errors are the API. Define
ErrNotFound,ErrInvalidat the service layer. Handlers check witherrors.Is. - Log once, at the top. Recoverer middleware logs the chain; lower layers don't
slog.Errorand re-return.
Example#
go
// service
func (s *Posts) GetBySlug(ctx context.Context, slug string) (*Post, error) {
p, err := s.repo.GetBySlug(ctx, slug)
if errors.Is(err, pgx.ErrNoRows) {
return nil, fmt.Errorf("get post %q: %w", slug, ErrNotFound)
}
if err != nil { return nil, fmt.Errorf("get post: %w", err) }
return p, nil
}
// handler
if err != nil { httpx.MapError(w, err); return }What I stopped doing#
- Wrapping at every call site. The stack is already in the trace.
- Custom error types with stack fields.
%wchains pluserrors.Isis enough. pkg/errors. The stdlib finally has what it gave us.